happens-before中 volatile 原则详解

前言:本篇文章中主要讲解 happens-before 中关于 volatile 原则的理解。
volatile 变量规则:对一个volatile域的写,happens-before 于任意后续对这个 volatile 域的读。

一、volatile 关键字的作用:

  1. 可见性:一个线程对共享变量的修改,另一个线程获取到的值一定是修改后的。
    测试代码如下:
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 关键字解决,大家可以自己验证。
happens-before中 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步:

  1. 为 ViewModelManager 分配内存空间
  2. 初始化 ViewModelManager
  3. 赋值给 mInstance

但是步骤2步骤3是可以交换顺序的,这就导致其他线程获取到的ViewModelManager未初始化,导致功能异常。

二、volatile 内存屏障

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM 采取保守策略。下面是基于保守策略的 JMM 内存屏障插入策略:

在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

下面是保守策略下,volatile 插入内存屏障后生成的指令序列示意图:
happens-before中 volatile 原则详解
注意StoreStore 保证上面的普通写操作对共享变量的修改已刷回主存。

volatile 插入内存屏障后生成的指令序列示意图:
happens-before中 volatile 原则详解
可以简单的归纳为如下的表格:
happens-before中 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。
       }
   }
}
上一篇:Re从零开始的每一天


下一篇:阿里云可申请试用云产品类型、配置及时长汇总