前言:本篇文章中主要讲解 happens-before 中关于 volatile 原则的理解。
volatile 变量规则:对一个volatile域的写,happens-before 于任意后续对这个 volatile 域的读。
一、volatile 关键字的作用:
- 可见性:一个线程对共享变量的修改,另一个线程获取到的值一定是修改后的。
测试代码如下:
public class TestVolatile {
static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
new Thread("线程1") {
@Override
public void run() {
while (!stop) {
}
System.out.println("线程停下来了");
}
}.start();
TimeUnit.MILLISECONDS.sleep(200);
stop = true;
System.out.println("需要停下来 >>> " + stop);
}
}
可以看到 主线程休眠200毫秒之后,设置 stop = ture
,但是线程1根本没停下来,这就是可见性问题。
可以通过在 变量 stop
前面加上 volatile
关键字解决,大家可以自己验证。
2. 禁止指令重排序
经典的例子就是 DCL 单例:
class ViewModelManager {
private ViewModelManager(){
}
private static volatile ViewModelManager mInstance;
public static ViewModelManager getInstance(){
if (mInstance == null){
synchronized (ViewModelManager.class){
if (mInstance == null)
mInstance = new ViewModelManager(); // 注释【1】
}
}
return mInstance;
}
}
// 注释【1】可以分为3步:
- 为 ViewModelManager 分配内存空间
- 初始化 ViewModelManager
- 赋值给 mInstance
但是步骤2和步骤3是可以交换顺序的,这就导致其他线程获取到的ViewModelManager未初始化,导致功能异常。
二、volatile 内存屏障
为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM 采取保守策略。下面是基于保守策略的 JMM 内存屏障插入策略:
在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
下面是保守策略下,volatile 写插入内存屏障后生成的指令序列示意图:
注意:StoreStore
保证上面的普通写操作对共享变量的修改已刷回主存。
volatile 读插入内存屏障后生成的指令序列示意图:
可以简单的归纳为如下的表格:
从上面的表格可以看出一下几点:
(1)当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
(2)当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
(3)当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
三、理解什么是 happens-before 原则中的 volatile 规则
volatile 规则 :对一个volatile变量的写,happens-before 于任意后续对这个 volatile 变量的读。
举个例子:
class TestVolatile {
private int a = 0;
private volatile boolean flag = false;
// 线程1
public void write() {
a = 1; // 注释【1】
flag = true; // 注释【2】
// 根据volatile写的内存屏障:volatile 写之前的操作禁止重排序到 volatile 写之后,
// 且 volatile 写之后会将变量 flag 刷回主存,即 a = 1的值也会被刷回主存
}
// 线程2
public void read() {
if (flag) { // 注释【3】
int i = a; // 注释【4】
// 根据volatile读的内存屏障:volatile 读之后的操作禁止重排序到volatile读之前。
// 所以这里面就会禁止指令重排序,只有 flag = true的时候才会将a的值赋给i。
}
}
}