volatile
读写屏障Java并发编程:volatile关键字解析 - Matrix海子 - 博客园
volatile 关键字在 Java 内存模型(JMM)中主要通过 内存屏障(Memory Barrier) 来保证变量的可见性和有序性(禁止指令重排序)。
HotSpot 虚拟机在汇编层面通常使用 lock 前缀指令(如 lock addl)来实现,这相当于一个全能型的屏障。但在 JMM 的规范描述中,我们将屏障分为四类:LoadLoad, StoreStore, LoadStore, StoreLoad。
以下是 volatile 读写操作插入屏障的具体规则:
1. volatile 写操作(Write)
对于 volatile 变量的写操作,JMM 会采取“前后夹击”的策略,确保写操作不会和前后的指令乱序,并且写完的数据能立马刷回主内存。
前面插入:StoreStore 屏障
- 位置:在普通写指令之后,
volatile写指令之前。 - 作用:禁止上面的普通写和下面的
volatile写重排序。 - 通俗解释:“在我(volatile写)动笔之前,前面所有的普通写入操作必须全部完成并刷到内存里,保证我写的时候,前面的数据是确定的。”
- 位置:在普通写指令之后,
后面插入:StoreLoad 屏障 (最核心、开销最大)
- 位置:在
volatile写指令之后,任意后续读/写指令之前。 - 作用:禁止上面的
volatile写和下面可能出现的volatile读/写重排序。 - 通俗解释:“在我写完之后,必须把数据强制刷回主内存,并且让其他 CPU 的缓存失效。在做完这步之前,后面谁也不许动。”
- 位置:在
结构示意:
[普通 Write]
[StoreStore 屏障] <-- 拦住,不让上面的普通写掉下来
[volatile Write] <-- 关键动作
[StoreLoad 屏障] <-- 拦住,不让下面的读写钻上去(最强屏障)
[任意 Read/Write]2. volatile 读操作(Read)
对于 volatile 变量的读操作,屏障主要插在后面,防止读取到的值被后面的操作“污染”或乱序使用。
后面插入:LoadLoad 屏障
- 位置:在
volatile读指令之后,后续普通读指令之前。 - 作用:禁止上面的
volatile读和下面的普通读重排序。 - 通俗解释:“必须先读我(volatile变量),读完之后,才能去读后面那些普通变量。”
- 位置:在
后面插入:LoadStore 屏障
- 位置:在
volatile读指令之后,后续普通写指令之前。 - 作用:禁止上面的
volatile读和下面的普通写重排序。 - 通俗解释:“必须先读我,读完后,你们才能进行后面的写入操作。”
- 位置:在
结构示意:
[volatile Read] <-- 关键动作
[LoadLoad 屏障] <-- 拦住,不让下面的普通读钻上去
[LoadStore 屏障] <-- 拦住,不让下面的普通写钻上去
[普通 Read/Write]3. 为什么这样设计?(实战场景:双重检查单例)
最经典的例子是 DCL(Double Checked Locking)单例模式:
instance = new Singleton();这行代码其实分三步:
memory = allocate();// 分配内存ctorInstance(memory);// 初始化对象instance = memory;// 引用赋值(volatile 写)
如果没有屏障,步骤 2 和 3 可能重排序。变成了“先赋值引用(非空),再初始化”。 另一个线程可能读到了非空的 instance(volatile 读),但此时对象还没初始化完(半成品),导致崩溃。
屏障如何解决?
- StoreStore 屏障(在 volatile 写之前):确保步骤 2(初始化)必须在步骤 3(赋值给 volatile 变量)之前完成。
- 这就完美禁止了指令重排,保证了对象发布的安全。
总结
- 写屏障:写前 StoreStore(防普通写乱序),写后 StoreLoad(防任何后续操作乱序,并强制刷内存)。
- 读屏障:读后 LoadLoad(防读乱序),读后 LoadStore(防写乱序)。
简单记忆:
- 写的时候,我很霸道,前面的事必须做完,后面的事不许抢跑。
- 读的时候,我很谨慎,必须先读到最新的我,然后才能做后面的事。
例子
在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
可能上面说的比较绕,举个简单的例子:
//x、y为非volatile变量``//flag为volatile变量` `x = ``2``; ``//语句1``y = ``0``; ``//语句2``flag = ``true``; ``//语句3``x = ``4``; ``//语句4``y = -``1``; ``//语句5由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
那么我们回到前面举的一个例子:
//线程1:``context = loadContext(); ``//语句1``inited = ``true``; ``//语句2` `//线程2:``while``(!inited ){`` ``sleep()``}``doSomethingwithconfig(context);前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。
实现原理
4.volatile的原理和实现机制
前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
使用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile` `boolean` `flag = ``false``;` `while``(!flag){`` ``doSomething();``}` `public` `void` `setFlag() {`` ``flag = ``true``;``}volatile` `boolean` `inited = ``false``;``//线程1:``context = loadContext(); ``inited = ``true``; ` `//线程2:``while``(!inited ){``sleep()``}``doSomethingwithconfig(context);2.double check
class` `Singleton{`` ``private` `volatile` `static` `Singleton instance = ``null``;`` ` ` ``private` `Singleton() {`` ` ` ``}`` ` ` ``public` `static` `Singleton getInstance() {`` ``if``(instance==``null``) {`` ``synchronized` `(Singleton.``class``) {`` ``if``(instance==``null``)`` ``instance = ``new` `Singleton();`` ``}`` ``}`` ``return` `instance;`` ``}``}