当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作肯定已经全部进行,且对后面的操作可见,在其后面的操作肯定还没有进行
在进行指令优化时,不能将 volatile 变量之前的语句放在对 volatile 变量的读写操作之后,也不能把 volatile 变量后面的语句放到其前面执行
举个栗子:
x=0; // 1
y=1; // 2
volatile z = 2; // 3
x=4; // 4
y=5; // 5
变量z为 volatile 变量,那么进行指令重排序时,不会将语句 3 放到语句 1、语句 2 之前,也不会将语句 3 放到语句 4、语句 5 后面。但是语句 1 和语句 2、语句 4 和语句 5 之间的顺序是不作任何保证的,并且 volatile 关键字能保证,执行到语句 3 时,语句 1 和语句 2 必定是执行完毕了的,且语句 1 和语句 2 的执行结果是对语句 3、语句 4、语句 5是可见的。
回到之前的例子:
// 线程1
String config = initConfig(); // 1
volatile boolean inited = true; // 2
// 线程2
while(!inited){
sleep();
}
doSomeThingWithConfig(config);
之前说这个例子提到有可能语句2会在语句1之前执行,那么就可能导致执行 doSomThingWithConfig() 方法时就会导致出错。
这里如果用 volatile 关键字对 inited 变量进行修饰,则可以保证在执行语句 2 时,必定能保证 config 已经初始化完毕。
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 到底如何保证可见性和禁止指令重排序的。
在《深入理解Java虚拟机》这本书中说道:
观察加入volatile关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令。
接下来举个栗子:
volatile 的 Integer 自增(i++),其实要分成 3 步:
读取 volatile 变量值到 local
增加变量的值
把 local 的值写回,让其它的线程可见
这 3 步的 JVM 指令为: