volatile的作用
1.保证变量的可见性
2.局部限制指令重排(指令重排存在多种情况:1.编译器重排;2.处理器并行重排;3.因为store buffer,invalid queue等异步机制的存在导致的内存重排)
为了达到这两个目的,volatile做了两件事情:
1.禁止编译器的优化和重排
2.通过内存屏障限制处理器重排
下面简单梳理下volatile如何限制编译器和处理器的重排。
首先明确下排序规则,简单总结如下(参考JSR-133)
a.第二个操作为volatile写,则无论第一个操作是什么,都无法重排序;
b.第一个操作为volatile读,则无论第二个操作是什么,都无法重排序;
c.第一个操作为volatile写,第二个操作为volatile读,无法重排序;
如何限制编译器指令重排
规则c保证了volatile变量自身的可见性,abc一起保证了操作的局部有序以及一些跨线程数据依赖的正确性。在某些情况下,不仅仅要求volatile变量本身的可见性,也需要有序性来保证语义的正确,如下面的典型场景:
两个变量:
int x=0;
int y=0;
线程A执行:
y=1 //1
x=1 //2
线程B执行:
while(x==1) { //3
assert y==1; //4
}
按照程序的语义,最后的断言应该是通过,但实际情况并非如此。原因有两个:1.如果没有a限制,操作1和操作2之前可能会被重排序,操作3的条件成立时,操作1可能还未执行,导致断言失败;2.编译器会对线程B的代码做优化,因为编译器无法判断跨线程的数据依赖,单独从线程B的视角看,x是不会变化的,为了提升性能,while(x==1)为直接被替换为while(false),永远无法执行操作4。
解决方案就是将变量x声明为volatile,限制的编译器的优化和操作1、操作2的重排,同时保证操作2的结果对操作3立即可见。
通过内存屏障限制处理器重排
四类内存屏障:LoadLoad、StoreStore、StoreLoad、LoadStore
volatile的内存屏障策略:
volatile写之前插入StoreStore屏障;(规则a,防止重排)
volatile写之后插入StoreLoad屏障;(规则c,保障可见性)
volatile读之后插入LoadStore屏障;(规则b,防止重排)
volatile读之后插入LoadLoad屏障;(规则b,防止重排)
在补充一些相关知识点,可能比较混乱,但个人觉得有助于对内存屏障的理解
1.StoreLoad的主要目的是保证volatile变量自身写后读的可见性,在实现上开销最大。
2.内存屏障作为一种逻辑抽象,具体实现有多种,比如x86下有mfence和lock指令,JVM选择了lock,及在写volatile变量时锁缓存总线,并且让其他CPU上对应缓存行的数据失效。
3.x86下只有StoreLoad屏障是有效操作,其他的屏障均为no-op。主要原因有二:1. x86中store-buffer是一个FIFO队列,结构上保证了写入顺序;2. x86中没有invalid queue,因此不需要LoadLoad或LoadStore来强制消费失效队列。
网友评论