一、为什么存在可见性问题?
可见性:当一个线程修改了一个数值后,其他线程应该立马看到。
为什么会存在该问题呢?
由于现在的计算机都拥有多个CPU,每个CPU都拥有L1、L2缓存,同时共享L3缓存,L3和内存进行交互。所以,有些时候,CPU之间由于缓存的存在,会有一些数据不一致。
为了保证CPU缓存之间数据的一致性,很多缓存一致性协议就被提了出来,例如MESI协议。MESI总结起来就是每个CPU都会通过嗅探在总线上传播的数据来检查自己的数据是否过期了,如果过期了,则将自己的数据设置为Invalid,下次使用时,强制从内存读取。
但是,缓存一致性协议使得性能下降了。所以,CPU设计者又进行了各种优化,比如在CPU和L1缓存之间加入了各级寄存器。所以,当CPU更改了一个数据后,会先写入寄存器(Buffer),然后由寄存器异步写入L1缓存(存在可见性问题),然后L1缓存同步写入主内存(由MESI协议保证)。
所以,还是会存在一些内存可见性的问题。
二、重排序与内存可见性的关系?
首先,我们先看一下重排序的种类:
- 编译器重排序:对没有依赖关系的语句,编译器重新调整语句的执行顺序。
- CPU指令重排序:在CPU指令级别,让没有依赖关系的指令并行
- CPU内存重排序:由于CPU有自己的buffer和缓存,由于异步写入,造成指令的执行顺序与写入内存的顺序不完全一致。
所以,重排序是在多线程条件下造成了内存可见性问题。
三、as-if-serial语义
因此,我们在什么时候可以重排序?什么时候不可以呢?
在单线程条件下,不管如何重排序,CPU和编译器都会保证程序的运行结果和我们预想的一样,即代码看起来像完全穿行的执行的,这就是as-if-serial语义。
但是,在多线程条件下,CPU和编译器只能保证每个单线程的as-if-serial语义,但是当现成之间存在交互的时候或者访问同一个变量的时候,就需要我们在CPU和编译器之上来决定何时可以重排序,什么时候不可以。
而我们是如何来告知CPU和编译器何时进行重排序,何时不进行的呢? 那就是通过happen-before语义来实现的。
四、happen-before
happen-before描述了两个操作之间的可见性。例如A happend-before B,意味着A的执行结果一定对B可见。这并不意味着A一定比B先执行,而是如果A比B早执行了,则A的结果一定对B可见。如果B先执行了,产生的错误需要我们去同步解决。
五、happen-before的实现(内存屏障)
happen-before的实现原理即借助内存和编译器提供的内存屏障。
cpu层面的内存屏障:
- LoadLoad:禁止读读重排
- StoreStore:禁止写写重排
- LoadStore:禁止读和写重排
- StoreLoad:禁止写和读的重排
java的unsafe()类中提供了三种内存屏障:
- loadFence:LoadLoad + LoadStore
- storeFence:StoreStore + LoadStore
- fullFence:四种加在一起
六、Volatile的实现原理
在volatile写操作之前加一个StoreStore屏障,保证该变量写的串行化,在后面加一个StoreLoad屏障,保障后面的读操作读取的是最新的值。
在volatile读操作之后加一个LoadStore屏障和一个LoadLoad屏障,保证读操作不会和后面的读操作和写操作不会发生重排序。
通过java并发编程(4)-----内存模型这篇文章我们知道,CPU的MESI模型是各个核通过嗅探来检查自己的缓存中的数据是否过期,但是虽然嗅探到了数据过期了,但是CPU由于存在指令重排,所以导致后面的指令到前面去,此时另一个CPU更新的数据还未写回主存,当前CPU则从自己缓存行中取了数据,导致并发错误。所以内存屏障的含义是配合缓存一致性协议,保证各个CPU数据的可见性。挡住当前CPU的指令,防止指令重排,以等待别的CPU发来消息,将自己缓存行中对应的数据设置为Invalid。
还是不太懂。。。等以后有机会再找找有没有参考书吧
有个问题,如果两个CPU同时修改了一个数据呢??这时候该怎么办?
参考