在阅读本文前,建议先看下面两篇文章:
Java 中 volatile 是轻量级的 synchronized,不会引起线程上下文的切换和调度。虽然 volatile 关键字只能修饰变量,但是要用好它并不容易,下面分析它的原理及注意事项。
volatile 关键字的两层语义
保证变量对所有线程的可见性,即一个线程修改了某个值,新值对其他线程来说是立即可见的
禁止指令重排序(Java 5 后才有的)
volatile 的可见性保证
要明白可见性语义,需要先了解 Java 内存模型的概念,具体可看 Java 内存模型,简单的说就是一个公共变量在每个线程都有线程私有的副本,所以正常情况下,一个线程修改了某个变量的值,只是修改其副本,其他线程不能得知该值的修改。
先看下面这段代码:
|
|
线程1 先执行count = 1;
,然后线程2 中读取count
的值时可能为 0 也可能为 1。因为线程2 无法知道线程1对count
变量的修改,但是也不确定的是也不知道该修改什么时候同步到共享内存,不知道何时把共享内存的值更新到线程2 的工作内存中。
而 volatile 关键字修饰变量后,每个线程对 volatile 变量的读取都要从共享内存中读,而每个线程对 volatile 变量的写不仅要更新工作内存还要刷新共享内存。而普通变量没法保证 JVM 何时把工作内存的值更新到共享内存,以及把共享内存的值读取到工作内存。所以用 volatile 关键字修饰count
变量后,线程2 就可以知道线程1 对其的修改了。
volatile 关键字不具备原子性
从上面知道 volatile 关键字保证了操作的可见性,但是 volatile 能保证对变量的操作是原子性吗?
看下面这个例子:
|
|
上面这段程序的输出结果很多朋友会认为是 10000,因为每个线程自增 1000 次,10线程的话就是 自增 10000 次,结果就是 10000。但是实际上它每次的运行结果都不一定一致,但都是一个小于 10000 的数值。
原因是 volatile 关键字保证了 count 变量的可见性,但是并没有保证increase()
方法的原子性。自增操作是不具备原子性的,编译成 class 文件的话,它分成三个指令:读取变量的值、进行加 1 操作、写入工作内存(因为是 volatile 变量,所以也会同步到共享内存)。大家可以想象一下这个场景:
某一时刻 count 的值为 10, 线程1对其进行自增操作,读取了值后就被阻塞了,切换到线程2对其进行自增操作,读取到的值为 10,然后进行加 1 操作并写入工作内存,同时同步到共享内存。然后切换回线程1,虽然共享内存中 count 的值为 11, 但是线程1下面的操作没有去读 count 的值,线程1的副本中 count的值还是 10,所以继续自增操作的下面两步后也把 11 更新到共享内存。这样执行两次自增操作,结果 count 只增加了 1。volatile 只能保证每次读取的是最新的值,但是线程没读取时是不知道其他线程修改的值的。
上面的问题的根源就是自增操作不是原子性操作,有下面几种方式把它变为原子性:(1)用 synchronized 修饰 increase() 方法(2)使用 ReentrantLock 把自增操作加锁(3)把 count 改为 AtomicInteger 类型,用 getAndIncrement() 原子操作自增。
volatile 禁止指令重排序
volatile 关键字能禁止指令重排序,所以 volatile 能在一定程度上保证有序性。
volatile 关键字禁止指令重排序有两层含义:
当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行
在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行
看下单例模式的双重检查锁定写法:
|
|
为什么sInstance
需要 volatile 关键字修饰,如果不修饰会可能会出现得到未初始化完成的对象,原因就是因为指令重排序。把sInstance = new Singleton();
换成字节码伪代码:
|
|
但是在一些 JIT 编译器上,2 和 3 可能会重排序,变成下面这样:
|
|
重排序后,sInstance 可能会为未调用构造函数的对象,假设线程1 执行到上面代码第二步时,还没构造完成,但是 sInstance 非空,切换到线程2 调用getInstance()
方法,因为 sInstance 非空就直接返回了,这样就会使用还没构造完成的对象。
但是用 volatile 关键字修饰了 sInstance 变量后,语句3 是 volatile 变量的写操作,不能将在对 volatile 变量访问的语句放在其后面执行,也就不允许语句2 放在语句3 后面,这样就能避免返回还未初始化完成的对象。
volatile 的实现原理
下面这段话摘自《深入理解 Java 虚拟机》:
“观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令”
lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供 3 个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他 CPU 中对应的缓存行无效。
参考文章