volatile关键字的语义
1.内存可见性
- 当一个线程对volatile修饰的变量进行写操作时,JMM会立即把线程对应的本地内存中的共享变量的值刷新到主内存
- 当一个线程对volatile修饰的变量进行读操作时,JMM会立即把该线程对应的本地内存置为无效,从主内存中读取共享变量的值
2.禁止重排序
如果volatile变量与普通变量发⽣了重排序,虽然volatile变量能保证内存可⻅性,但是可能导致普通变量读取错误
JVM通过内存屏障来实现限制处理器的重排序。编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
编译器选择了⼀个⽐较保守的JMM内存屏障插⼊策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:
- 在每个volatile写之前插入一个StoreStore屏障
- 在每个volatile写之后插入一个StoreLoad屏障
- 在每个volatile读之后插入一个LoadLoad屏障
- 在每个volatile读之后再插入一个LoadStore屏障
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写⼊操作执⾏前,保证Store1的写⼊操作对其它处理器可⻅。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写⼊操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执⾏前,保证Store1的写⼊对所有处理器可⻅。它的开销是四种屏障中最⼤的(冲刷写缓冲器,清空⽆效化队列)。在⼤多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
volatile和普通变量重排序规则总结
-
如果第一个操作是volatile读,那么不管第二个操作是什么,都不能重排序
-
如果第二个操作是volatile写,那么不管第一个操作是什么,都不能重排序
-
如果第一个操作是volatile写,第二个操作是volatile读,那么不能重排序
如果第一个操作是普通变量读,第二个操作是volatile读,那么可以重排序,如下:
// 声明变量
int a = 0; // 声明普通变量
volatile boolean flag = false; // 声明volatile变量
// 以下两个变量的读操作是可以重排序的
int i = a; // 普通变量读
boolean j = flag; // volatile变量读
volatile的用途
从volatile的内存语义上来看,volatile可以保证内存可⻅性且禁⽌重排序。在保证内存可⻅性这⼀点上,volatile有着与锁相同的内存语义,所以可以作为⼀个“轻量级”的锁来使⽤。
但由于volatile仅仅保证对单个volatile变量的读/写具有原⼦性,⽽锁可以保证整个临界区代码的执⾏具有原⼦性。
所以在功能上,锁⽐volatile更强⼤;在性能上,volatile更有优势
volatile可以用于单例模式的双检锁检查写法
如果这⾥的变量声明不使⽤volatile关键字,是可能会发⽣错误的。它可能会被重排序:
instance = new Singleton(); // 第10⾏
// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址
// 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象
⽽⼀旦假设发⽣了这样的重排序,⽐如线程A在第10⾏执⾏了步骤1和步骤3,但是步骤2还没有执⾏完。这个时候线程A执⾏到了第7⾏,它会判定instance不为空,然后直接返回了⼀个未初始化完成的instance!
参考资料
本文参考于这里