避免重排序之使用 Volatile 关键字

简介: volatile 关键字本身就包含了禁止指令重排序的语义,仅保证赋值过程的原子性,保障变量的可见性,但不具备排它性
我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,我们互相鼓励

欢迎关注微信公众号「架构染色」交流和学习

Volatile 关键字有以下应用场景:

  • long /double 类型的原子性读
  • 防止指令重排
  • 保证变量的可见性

一、读写屏障

运行期重排序:内存系统的重排序
中有提到 store buffer 和   Invalidate Queue 带来了乱序,CPU 提供了内存屏障指令,来解决这样的乱序问题。读屏障,清空本地的 invalidate queue,保证之前的所有 load 都已经生效;写屏障,清空本地的 store buffer,使得之前的所有 store 操作都生效。

1.1 JMM 视角

JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。JMM 通过内存屏障阻止这种重排序,两个维度了解内存屏障:

  1. 由于编译器的优化和缓存的使用, 导致对内存的写入操作不能及时的反应出来, 也就是说当完成对内存的写入操作之后, 读取出来的可能是旧的内容。
  2. 内存屏障是一种指令, 对该指令之前和之后的内存 CPU 读写内存的操作, 产生一种顺序的约束. 内存屏障在一定程度上和一定范围里阻止指令的乱序, 从而阻止了 CPU 的乱序执行。

Java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。

二、 volatile 避免重排序

2.1 JMM 针对编译器制定 volatile 重排序规则表

读写的类型分为:

  • 无 volatile 关键字的变量的读写(普通读写)
  • volatile 关键字修饰的变量的读(volatile 读)
  • volatile 关键字修饰的变量的写(volatile 写)

对有 volatile 关键字所修饰的变量的读写操作,其前后其他的读写操作的重排优化要注意(必须保证可见性)

image.png

上图中 NO 是禁止重排:

  1. 当第二个操作是 volatile 写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。
  2. 当第一个操作是 volatile 读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。
  3. 当第一个操作是 volatile 写,第二个操作是 volatile 读时,不能重排序。

2.2 指令序列中插入内存屏障来禁止特定类型的处理器重排序

为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,JMM 采取了保守的内存屏障插入策略,不求最优,但求不错:

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

屏障的介绍:
image.png

1)volatile 写是在前面和后面分别插入内存屏障

image.png

2)volatile 读操作是在后面插入两个内存屏障。

image.png

2.3 happens-before 原则指定 volatile 变量规则

对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

三、经典使用场景:双重检查(double-checked)单例模式

几个主要环节可能产生重排序:

  1. 分配内存
  2. 初始化内存
  3. 地址赋值给变量

若 2,3 发生重排序,可能出现拿到了地址,但是数据还未初始化好。
所以不能忽略 volatile 关键字

class Singleton {
    private volatile static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            syschronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

上边这种写法可以说对知识点的要求有点多,为了避免出错,可考虑使用懒加载优雅写法 Initialization on Demand Holder(IODH)。

public class Singleton {
    static class SingletonHolder {
        static Singleton instance = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

四、volatile 与 synchronized 的区别

  1. 首先 volatile 关键字本身就包含了禁止指令重排序的语义,而 synchronized(及其它的锁)是通过“一个变量在同一时刻只允许一条线程对其进行 lock 操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块智能串行的进入;
  2. synchronized 可以保证原子性操作,对任意单个 volatile 变量的简单读/写具有原子性,volatile 仅保证赋值过程的原子性,而赋值前的运算不在其职责范围:
    2.1 复合操作不具有原子性,类似于 a++; B b = new B(); .
    2.2 对于原子性写操作,=号右边的赋值变量中不能出现多线程共享的变量,即便这个变量是 volatile 修饰的,也不行。
  3. 结合 1 和 2 来可抽取出这个结论: volatile 可确保变量写操作的原子性(比如 32 位下的 long/double 本是分 2 次 32 位操作,没有原子性;加了 volatile 后其读写具有原子性效应),但不具备排它性。锁会导致线程的上下文在 操作系统内核态和用户态之间的切换;volatile 是在用户态执行,不会切换。
  4. 都可以保证变量的可见性(比如程序读取变量的值是在寄存器,高速缓存中的缓存,而非变量当时正确的值)
  5. 都可防止重排序

参考并感谢

【Java 并发笔记】volatile 相关整理
http://ifeve.com/from-singleton-happens-before/

相关文章
|
7月前
|
缓存 编译器
volatile关键字
volatile关键字
|
7月前
|
缓存 编译器 C语言
一起来探讨volatile关键字
在C语言中,volatile是一个关键字,用于告诉编译器不要对被声明为volatile的变量做优化,以确保每次对该变量的读写都直接操作内存。
|
缓存 安全 Java
【volatile关键字】
【volatile关键字】
|
存储 缓存 Java
volatile 关键字说明
volatile 关键字说明
53 0
|
4月前
|
存储 Java 编译器
|
存储 Java
浅谈Volatile关键字
该篇文章用来总结笔者对于Volatile关键字的理解,并不会太过深入的探讨。
136 0
浅谈Volatile关键字
|
存储 缓存 Java
volatile关键字再理解
volatile关键字再理解
volatile关键字再理解
|
SQL 缓存 安全
深入理解volatile关键字
深入理解volatile关键字
196 0
深入理解volatile关键字
|
缓存 前端开发 Java
volatile关键字有什么用?
volatile关键字有什么用?
volatile关键字有什么用?