你真的了解 volatile 关键字吗?

今天,让我们一起来探讨 Java 并发编程中的知识点:volatile 关键字

本文主要从以下三点讲解 volatile 关键字:

  1. volatile 关键字是什么?
  2. volatile 关键字能解决什么问题?使用场景是什么?
  3. volatile 关键字实现的原理?

volatile 关键字是什么?

在 Sun 的 JDK 官方文档是这样形容 volatile 的:

The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes. A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.

也就是说,如果一个变量加了 volatile 关键字,就会告诉编译器和 JVM 的内存模型:这个变量是对所有线程共享的、可见的,每次 JVM 都会读取最新写入的值并使其最新值在所有 CPU 可见。volatile 可以保证线程的可见性并且提供了一定的有序性,但是无法保证原子性。在 JVM 底层 volatile 是采用内存屏障来实现的。

通过这段话,我们可以知道 volatile 有两个特性:

  1. 保证可见性、不保证原子性
  2. 禁止指令重排序

原子性和可见性

原子性是指一个操作或多个操作要么全部执行并且执行的过程不会被任何因素打断,要么都不执行。性质和数据库中事务一样,一组操作要么都成功,要么都失败。看下面几个简单例子来理解原子性:

i == 0;       //1
j = i;        //2
i++;          //3
i = j + 1;    //4

在看答案之前,可以先思考一下上面四个操作,哪些是原子操作?哪些是非原子操作?

答案揭晓:

1——是:在Java中,对基本数据类型的变量赋值操作都是原子性操作(Java 有八大基本数据类型,分别是byte,short,int,long,char,float,double,boolean)
2——不是:包含两个动作:读取 i 值,将 i 值赋值给 j
3——不是:包含了三个动作:读取 i 值,i+1,将 i+1 结果赋值给 i
4——不是:包含了三个动作:读取 j 值,j+1,将 j+1 结果赋值给 i

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

注:由于以前的操作系统是 32 位, 64 位数据(long 型,double 型)在 Java 中是 8 个字节表示,一共占用 64 位,因此需要分成两次操作采用完成一个变量的赋值或者读取操作。随着 64 位操作系统越来越普及,在 64 位的 HotSpot JVM 实现中,对64 位数据(long 型,double 型)做原子性处理(由于 JVM 规范没有明确规定,不排除别的 JVM 实现还是按照 32 位的方式处理)。

在单线程环境中我们可以认为上述步骤都是原子性操作,但是在多线程环境下,Java 只保证了上述基本数据类型的赋值操作是原子性的,其他操作都有可能在运算过程中出现错误。为此在多线程环境下为了保证一些操作的原子性引入了锁和 synchronized 等关键字。

上面说到 volatile 关键字保证了变量的可见性,不保证原子性。原子性已经说了,下面说下可见性。

可见性其实和 Java 内存模型的设定有关:Java 内存模型规定所有的变量都是存在主存(线程共享区域)当中,每个线程都有自己的工作内存(私有内存)。线程对变量的所有操作都必须在工作内存中进行,而不直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

举个简单栗子:

比如上面 i++ 操作,在 Java 中,执行 i++ 语句:

执行线程首先从主存中读取 i(原始值)到工作内存中,然后在工作内存中执行运算 +1 操作(主存的 i 值未变),最后将运算结果刷新到主存中。

数据运算是在执行线程的私有内存中进行的,线程执行完运算后,并不一定会立即将运算结果刷新到主存中(虽然最后一定会更新主存),刷新到主存动作是由 CPU 自行选择一个合适的时间触发的。假设数值未更新到主存之前,当其他线程去读取时(而且优先读取的是工作内存中的数据而非主存),此时主存中可能还是原来的旧值,就有可能导致运算结果出错。

以下代码是测试代码:

package com.wupx.test;

/**
 * @author wupx
 * @date 2019/10/31
 */
public class VolatileTest {

    private boolean flag = false;

    class ThreadOne implements Runnable {
        @Override
        public void run() {
            while (!flag) {
                System.out.println("执行操作");
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("任务停止");
        }
    }

    class ThreadTwo implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(2000L);
                System.out.println("flag 状态改变");
                flag = true;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        VolatileTest testVolatile = new VolatileTest();
        Thread thread1 = new Thread(testVolatile.new ThreadOne());
        Thread thread2 = new Thread(testVolatile.new ThreadTwo());
        thread1.start();
        thread2.start();
    }
}

上述结果有可能在线程 2 执行完 flag = true 之后,并不能保证线程 1 中的 while 能立即停止循环,原因在于 flag 状态首先是在线程 2 的私有内存中改变的,刷新到主存的时机不固定,而且线程 1 读取 flag 的值也是在自己的私有

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

后端核心技术精讲 文章被收录于专栏

专注分享后端技术干货,包括 Java 基础、Java 并发、JVM、Elasticsearch、Zookeeper、Nginx、微服务、消息队列、源码解析、数据库、设计模式、面经等,助你编程之路少走弯路。

全部评论
原创不易,欢迎点赞、收藏、订阅三连!
点赞 回复 分享
发布于 2020-04-05 18:34

相关推荐

点赞 评论 收藏
分享
09-25 10:34
东北大学 Java
多面手的小八想要自然醒:所以读这么多年到头来成为时代车轮底下的一粒尘
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
昨天 10:52
点赞 评论 收藏
分享
1 1 评论
分享
牛客网
牛客企业服务