volatile关键字是什么?( 三 )


String config = initConfig();// 1
volatile boolean inited = true; // 2
// 线程2
while(!inited){
sleep();
}
doSomeThingWithConfig(config);
之前说这个例子提到有可能语句2会在语句1之前执行 。那么就可能导致执行 doSomThingWithConfig() 方法时就会导致出错 。
这里如果用 volatile 关键字对 inited 变量进行修饰 。则可以保证在执行语句 2 时 。必定能保证 config 已经初始化完毕 。
volatile 应用场景
synchronized 关键字是防止多个线程同时执行一段代码 。那么就会很影响程序执行效率 。而 volatile 关键字在某些情况下性能要优于 synchronized 。但是要注意 volatile 关键字是无法替代 synchronized 关键字的 。因为 volatile 关键字无法保证操作的原子性 。通常来说 。使用 volatile 必须具备以下三个条件:
对变量的写入操作不依赖变量的当前值 。或者能确保只有单个线程更新变量的值
该变量不会与其他状态变量一起纳入不变性条件中
在访问变量时不需要加锁
上面的三个条件只需要保证是原子性操作 。才能保证使用 volatile 关键字的程序在高并发时能够正确执行 。建议不要将 volatile 用在 getAndOperate 场合 。仅仅 set 或者 get 的场景是适合 volatile 的 。
常用的两个场景是:
状态标记量
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);
DCL 双重校验锁-单例模式
public class Singleton {
private volatile static Singleton instance = null;
private Singleton() {
}
/**
* 当第一次调用getInstance()方法时 。instance为空 。同步操作 。保证多线程实例唯一
* 当第一次后调用getInstance()方法时 。instance不为空 。不进入同步代码块 。减少了不必要的同步
*/
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
使用 volatile 的原因在上面解释重排序时已经讲过了 。主要在于 instance = new Singleton() 。这并非是一个原子操作 。在 JVM 中这句话做了三件事情:
给 instance分配内存
调用 Singleton 的构造函数来初始化成员变量
将 instance 对象指向分配的内存库存空间(执行完这步 instance 就为非 null 了)
但是 JVM 即时编译器中存在指令重排序的优化 。也就是说上面的第二步和第三步顺序是不能保证的 。最终的执行顺序可能是 1-2-3 。也可能是 1-3-2 。如果是后者 。线程 1 在执行完 3 之后 。2 之前 。被线程 2 抢占 。这时 instance 已经是非 null(但是并没有进行初始化) 。所以线程 2 返回 instance 使用就会报空指针异常 。
volatile 特性是如何实现的呢?
前面讲述了关于 volatile 关键字的一些使用 。下面我们来探讨一下 volatile 到底如何保证可见性和禁止指令重排序的 。
在《深入理解Java虚拟机》这本书中说道:
观察加入volatile关键字和没有加入 volatile 关键字时所生成的汇编代码发现 。加入 volatile 关键字时 。会多出一个 lock 前缀指令 。
接下来举个栗子:
volatile 的 Integer 自增(i++) 。其实要分成 3 步:
读取 volatile 变量值到 local
增加变量的值
把 local 的值写回 。让其它的线程可见
这 3 步的 JVM 指令为:
mov0xc(%r10),%r8d ; Load
inc%r8d; Increment
mov%r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier
lock 前缀指令实际上相当于一个内存屏障(也叫内存栅栏) 。内存屏障会提供 3 个功能:
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置 。也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时 。在它前面的操作已经全部完成(满足禁止重排序)
它会强制将对缓存的修改操作立即写入主存(满足可见性)
如果是写操作 。它会导致其他 CPU 中对应的缓存行无效(满足可见性)
volatile 变量规则是 happens-before(先行发生原则)中的一种:对一个变量的写操作先行发生于后面对这个变量的读操作 。(该特性可以很好解释 DCL 双重检查锁单例模式为什么使用 volatile 关键字来修饰能保证并发安全性)
总结

推荐阅读