happens-before中 volatile 原则详解

Posted 小猪快跑22

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 关键字解决,大家可以自己验证。
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 插入内存屏障后生成的指令序列示意图:

注意StoreStore 保证上面的普通写操作对共享变量的修改已刷回主存。

volatile 插入内存屏障后生成的指令序列示意图:

可以简单的归纳为如下的表格:

从上面的表格可以看出一下几点:
(1)当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
(2)当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
(3)当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

三、理解什么是 happens-before 原则中的 volatile 规则

happens-before来指定两个操作之间的执行顺序。

这两个操作可以在一个线程之内,也可以是在不同线程之间。因此JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果 A线程的写操作 a 与 B 线程的读操作 b 之间存在happensbefore关系,尽管 a 操作和 b 操作在不同的线程中执行,但JMM向程序员保证 a 操作将对 b 操作可见)

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。
       
   

以上是关于happens-before中 volatile 原则详解的主要内容,如果未能解决你的问题,请参考以下文章

happens-before规则

java面试总躲不过的并发:volatile原理 + happens-before原则

happens-before

Java 多线程:volatile 变量happens-before 关系及内存一致性

面试并发volatile关键字时,我们应该具备哪些谈资?

Android-JMM内存模型-指令重排-Happens-Before原则-volatile-lock指令-内存屏障