在面试的时候,volatile实在太容易遇到了,所以突然想总结一下里面比较关键的地方。
volatile如何实现禁止指令重排的?
首先先说一下什么是指令重排,假如有以下场景:
int a = 1;
int b = 1;
在jvm里面的顺序,可能是先执行初始化b,再初始化a,这就叫做指令重排。
那么,为什么jvm要指令重排?
指令重排的初衷是让代码在不改变执行结果的情况下,改变代码执行的顺序。这个的执行层面是操作系统级别的,而不是简单的jvm,你也可以理解为是cpu操作jvm字节码的时候,这么的一个操作。
指令重排带来了什么?
单线程情况下不明显,多线程情况下,必然会产生一定的问题。
volatile和cpu指令重排有什么关系呢?
被volatile修饰的变量将会禁止指令重排,比如说,如果被volatile修饰的变量在第五行生命出来,那么前四行的执行顺序就一定不会改变。
volatile是怎么做到禁止指令重排的呢?
首先先介绍一个概念,叫做内存屏障(memory Barrier):内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
内存屏障有哪几种呢?
内存屏障一共有四种:
①:LoadLoad屏障
抽象场景:Load1; LoadLoad; Load2
Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
②:StoreStore屏障
抽象场景:Store1; StoreStore; Store2
Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见
③:LoadStore屏障
抽象场景:Load1; LoadStore; Store2
在Store2被写入前,保证Load1要读取的数据被读取完毕。
④:StoreLoad屏障
抽象场景:Store1; StoreLoad; Load2
在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。
当一个变量被volatile修饰了之后,从jvm层面会对这个变量做两件事情:
①在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
②在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
由此可以证明得到volatile除了可见性以外的另一个特性:阻止编译时和运行时的指令重排。编译时JVM编译器遵循内存屏障的约束,运行时依靠CPU屏障指令来阻止重排。