Volatile关键字
内存可见性
为什么存在内存可见性问题
因为CPU缓存一致性协议,例如MESI,多个CPU之间的缓存不会出现不同步问题,也就不会有内存可见性问题。但是缓存一致性协议对性能有很大的损耗,为了解决这个问题,CPU的设计者们在这个基础上又进行了各种优化,例如在计算单元L1之间加了Store Buffer、Load Buffer(还有其他各种Buffer)L1、L2、L3和主存之间是同步的,有缓存一致性协议的保证,但是Store Buffer、Load Buffer和L1之间却是异步的。也就是说,往内存中写入一个变量,这个变量为惠存在Store Buffer里面,稍后才异步地写入L1中,同时同步写入主存中。
Store Buffer的延迟写入是重排序的一种,成为内存重排序,除此之外还有编译器和CPU指令重排序。
解决方案
内存可见性是写完之后立即对其他线程可见,他的反面是稍后才能看见。解决这个问题的方案是在变量前面加上volatile关键字。
禁止指令重排
只要操作之间没有数据依赖性,编译器和CPU都可以任意重排序,因为执行结果不会改变,代码看起来像完全穿行地一行行从头执行到尾,这也就是as-if-serial语义。
在多线程下,编译器和CPU只能保证每个线程的as-if-else语义,每个线程内部都是“看似完全串行的”,但是多个线程会互相读取和写入共享变量,对于这种互相影响,编译器和CPU不会考虑。
JMM引入了happen-before,使用happen-before保证两个操作之间的内存可见性。如果A happen-before B,意味着A的执行结果必须对B可见,也就是保证跨线程的内存可见性。A happen before B不代表A一定在B之前执行。以为对于多线程的程序而言,两个操作的执行顺序是不确定的。happen-before值确保如果A在B之前执行,则A的执行结果必须对B可见。
为了禁止编译器重排序和CPU重排序,在编译器和CPU层面都有对应的指令,也就是内存屏障(Memory Barrier)。这也正是JMM和happen-before规则的底层实现原理。
编译器的内存屏障,知识为了告诉编译器不要对指令进行重排序。当编译完成之后,这种内存屏障就消失了,CPU并不会感知到编译器中内存屏障的存在。
CPU的内存屏障就是CPU提供的指令,可以由开发者显式调用。
64位写入的原子性
JVM的规范没有要求64位的long或者double的写入是原子的。在32位及机器上,一个64位变量的写入可能被拆分成两个32位的写操作来执行。这样一来,读取的线程就可能读到”一般的值“。解决办法就是在long前面加上volatile关键字
volatile的实现原理
这里只探讨为了实现volatile关键字的语义的一种参考做法:
(1)在volatile写操作的前面插入一个StoreStore屏障。保证volatile写操作不会和之前的写操作重排序。
(2)在volatile写操作的后面插入一个StoreLoad屏障。保证volatile写操作不会和之后的读操作重排序。
(3)在volatile读操作的后面插入一个LoadLoad屏障+LoadStore屏障。保证volatile读操作不会和之后的读操作、写操作重排序。