volatile关键字解析

理解volatile关键字之前,建议先理解java内存模型(参考......)。

在并发编程中,难免会遇到共享数据并发处理的问题,这些问题主要体现在了并发编程需要注意的几个特性:原子性,可见性,有序性(参考......)。volatile能够保证其中的可见性和有序性(一定程度上),但不能保证原子性。
所以用volatile修饰一个共享变量A,那么对A就有了2层语义:
  1. 变量A在多线程处理中持有可见性,即线程1修改了变量A,那么其他线程可以立即看到变量A的新值。
  2. 禁止指令重排序(一定程度有序性的体现)。
下面我们分别理解下这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修饰符对这段代码有以下影响:
  1. 线程2对stop变量修改后会立即将新值更新到主内存;
  2. 线程2对stop变量修改,会导致线程1工作内存中的stop缓存变量的缓存行失效 (反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)
  3. 由于线程1的工作内存中的stop缓存变量的缓存行无效了,所以线程1再次获取stop的值时会从主内存读取。
这样线程2对stop变量作修改,在线程1中就能立刻更新到新值,并停止working,working,working......

禁止指令重排序:
这里涉及到指令重排序的概念,请参考......
volatile关键字的禁止指令重排序体现在:
  1. 当程序执行到用volatile修饰的变量的读/写操作时,在这个操作前面的其他操作肯定已经完成,且结果对后面的其他操作(肯定还没有执行)可见。
  2. 在处理器对代码指令进行优化时,不能将对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 变量提供理想的线程安全,必须同时满足下面两个条件:
  1. 对变量的写操作不依赖于当前值。
  2. 该变量没有包含在具有其他变量的不变式中。

以下是几个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);

double check(参考http://www.iteye.com/topic/652440):
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并发编程实践》
原文地址:https://www.cnblogs.com/ViviChan/p/4981717.html