并发编程(三十三)Volatile禁止指令重排序

103 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第31天,点击查看活动详情

在java文件编译成字节码时,volatile会在指令序列中加入内存屏障来禁止指令重排序。

下面是基于保守策略的JMM内存屏障插入策略。

  • 在每个volatile写操作的前面插入一个StoreStore屏障。(防止前面的store指令和当前的store指令重排序)

  • 在每个volatile写操作的后面插入一个StoreLoad屏障。(防止当前的store指令和后面的load指令重排序)

  • 在每个volatile读操作的后面插入一个LoadLoad屏障。(防止当前的load指令和后面的load指令重排序)

  • 在每个volatile读操作的后面插入一个LoadStore屏障。(防止当前的load指令和后面的store指令重排序)

1.保守策略下,volatile写插入内存屏障

保守策略下,volatile写插入内存屏障后生成的指令序列示意图

image.png

图中volatile写后面有一个StoreLoad屏障,作用是为了防止volatile写与后面可能有的volatile读/写操作重排序。

由于编译器无法判断volatile写后是否需要加入一个StoreLoad屏障,比如我代码是:volatile int a = 10;下一行就直接return。显然这个StoreLoad屏障是无意义的。但是JMM采取了最保守的策略:在每个volatile写的后面,或者在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM最终选择了在每个volatile写的后面插入一个StoreLoad屏障。为什么这样呢? 因为大部分情况下,都是去读取变量值,比如一个线程是更改volatile变量值,读取volatile线程数量很多。所以在volatile写后加入StoreLoad屏障会大大提高执行效率。

2.保守策略下,volatile读插入内存屏障

保守策略下,volatile读插入内存屏障后生成的指令序列示意图

image.png

3.不保守策略下,volatile读写插入内存屏障

上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面通过具体的示例代码进行说明。

class VolatileBarrierExample {
    int a;
    volatile int vl = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;// 第一个volatile读
        int j = v2;// 第二个volatile读
        a=i+j// 普通写
        vl =i+ 1;// 第一个volatile写
        v2 =j* 2;// 第二个volatile写
    }
    //其他方法
}

不保守策略下,volatile读写插入内存屏障后生成的指令序列示意图 image.png