理解volatile关键字之前,建议先理解java内存模型(参考......)。
在并发编程中,难免会遇到共享数据并发处理的问题,这些问题主要体现在了并发编程需要注意的几个特性:原子性,可见性,有序性(参考......)。volatile能够保证其中的可见性和有序性(一定程度上),但不能保证原子性。
所以用volatile修饰一个共享变量A,那么对A就有了2层语义:
- 变量A在多线程处理中持有可见性,即线程1修改了变量A,那么其他线程可以立即看到变量A的新值。
- 禁止指令重排序(一定程度有序性的体现)。
下面我们分别理解下这2层语义:
可见性:
下面我们先看一段简单的代码:
boolean stop = false; // 线程1 while(stop ) { working(); } // 线程2 stop = true ;
编写这段代码的本意是通过stop变量作为中断标志,控制线程1是否执行继续执行某个任务。理想的情况是线程2执行stop = true 后,线程1在执行完当前working()的任务后,立刻退出 while 循环。但是事实不太理想。这段代码有很大的不确定性。根据java内存模型的相关知识,我们知道线程1和2共享stop变量(在主内存中),并且在线程各自的缓存中有stop变量的副本。线程2执行stop = true ,计算机对该指令实际操作有:①从主内存读取stop变量保存到自己的工作内存(缓存)②修改工作内存中的stop变量副本值为true ③将工作内存中stop变量副本值(true)更新到主内存的共享stop变量中。
但这线程2有可能在执行完第 ② 条指令之后转去做其他事情,第 ③ 条指令迟迟未执行。线程1只能不断的working,working,working......直到线程2的第 ③ 条指令执行。
如果stop变量声明为:public volatile boolean stop = false; 就能达到我们用stop变量作为中断标志的本意。使用volatile修饰符对这段代码有以下影响:
- 线程2对stop变量修改后会立即将新值更新到主内存;
- 线程2对stop变量修改,会导致线程1工作内存中的stop缓存变量的缓存行失效 (反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
- 由于线程1的工作内存中的stop缓存变量的缓存行无效了,所以线程1再次获取stop的值时会从主内存读取。
这样线程2对stop变量作修改,在线程1中就能立刻更新到新值,并停止working,working,working......
禁止指令重排序:
这里涉及到指令重排序的概念,请参考......
volatile关键字的禁止指令重排序体现在:
- 当程序执行到用volatile修饰的变量的读/写操作时,在这个操作前面的其他操作肯定已经完成,且结果对后面的其他操作(肯定还没有执行)可见。
- 在处理器对代码指令进行优化时,不能将对volatile变量操作前的指令放在其后面执行,也不能将对volatile变量操作后的指令放在其前面执行。
先举个简单的例子:
int x ; int y ; volatile int z ; x = 0; // 语句1 y = 0; // 语句2 z = 666; // 语句3 x = 1; // 语句4 y = 1; // 语句5
由于 z 变量被volatile修饰,那么处理器对指令重排序的时候,不会将语句3放在语句1,语句2前面,也不会将语句3放在语句4,语句5后面。但语句1,语句2的执行顺序、语句4,语句5的执行顺序有可能乱序(因为执行顺序修改后结果是一致的)。并且volatile关键字能保证程序执行到语句3时,语句1,语句2肯定已经执行结束,语句4,语句5未执行,而且语句1,语句2的执行结果对语句4,语句5可见。
再举个用一个共享变量做多线程信号标记的例子:
// 线程1 context = initContext(); // 语句1 inited = true; // 语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
由于指令重排序,语句2有可能在语句1之前执行,那么线程2有可能用一个null的context去执行doSomethingwithconfig方法而导致程序出错。
如果inited变量用volatile修饰,那么就可以保证语句1在语句2之前执行,就能保证线程2正确执行。
实现原理:
前面讲述了源于volatile关键字的概念和使用,那么java虚拟机如何对volatile变量实现可见性和禁止指令重排?
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
使用场景:
在某些情况下,如果读操作远远大于写操作,volatile 变量可以提供优于其他锁的性能优势。
但是要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
以下是几个volatile常用的场景:
状态标识:
volatile boolean flag = false; while(!flag){ doSomething (); } public void setFlag() { flag = true; } volatile boolean inited = false; //线程1: context = initContext() ; inited = true; //线程2: while(!inited ){ sleep( ) } doSomethingwithconfig(context);
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; } }
注意事项:
volatile在多线程应用中并不是完全的线程安全(不能完全保证原子性,可见性,有序性),所以不能用volatile替代lock,synchronized之类的锁。
volatile不足以确保类似多线程进行count++操作的原子性,除非能保证只有一个线程进行count++。
引申:
volatile 变量具有
synchronized
的可见性特性,但是不具备原子特性。Java
语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized
”;与 synchronized
块相比,volatile
变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized
的一部分。参考资料:
《java并发编程实践》