避免重排序之使用 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 关键字所修饰的变量的读写操作,其前后其他的读写操作的重排优化要注意(必须保证可见性)

上图中 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 屏障。

屏障的介绍:

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

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

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. 都可防止重排
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值