开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第31天,点击查看活动详情
在java文件编译成字节码时,volatile会在指令序列中加入内存屏障来禁止指令重排序。
下面是基于保守策略的JMM内存屏障插入策略。
-
在每个volatile写操作的前面插入一个StoreStore屏障。(防止前面的store指令和当前的store指令重排序)
-
在每个volatile写操作的后面插入一个StoreLoad屏障。(防止当前的store指令和后面的load指令重排序)
-
在每个volatile读操作的后面插入一个LoadLoad屏障。(防止当前的load指令和后面的load指令重排序)
-
在每个volatile读操作的后面插入一个LoadStore屏障。(防止当前的load指令和后面的store指令重排序)
1.保守策略下,volatile写插入内存屏障
保守策略下,volatile写插入内存屏障后生成的指令序列示意图
图中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读插入内存屏障后生成的指令序列示意图
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读写插入内存屏障后生成的指令序列示意图