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读操作不会和之后的读操作、写操作重排序。