volatile为什么可以保证内存可见性及防止指令重排序?
内存
共享主存和高速缓存(工作内存)。CPU高速缓存(L1,2)产生原因读写主存没有CPU执行指令快,他是某个CPU独有,只与该CPU运行的线程有关。
内存可见性
简单的说,CPU对数据的修改,对其他CPU立刻可见。下面我们详细地说。
- CPU修改数据,首先对工作内存修改,再同步主内存。单线程中,变量在工作内存的副本一直有效,CPU不用每次修改从主存读取变量,只是每次修改后同步主存。
- 对其他CPU立刻可见。当一个CPU修改变量,同步主存,如果其他CPU的工作内存也缓存这个变量,那么这个CPU的变量失效,当这个CPU想修改变量,必须从主存重新获取变量。
volatile保证内存可见性基于MESI协议实现的。MESI协议是缓存一致性协议一种。
题外话,如何保持CPU缓存一致性。
第一种,操作系统在总线上发出lock信号,其他处理器既不能操作缓存了该共享变量内存地址的缓存,阻塞其他CPU,使该处理器可以独享此共享内存。总线锁定把CPU和内存的通信锁住,使得其他处理器在锁定期间不能操作其他内存地址的数据。
第二种,缓存一致性机制。当对某块CPU对缓存的数据操作后,通知其他CPU废弃存储在内部的缓存,或者从主存重新读取。
MESI协议
M:Modify,数据只在本CPU缓存,其他CPU没有,且数据修改没有更新到内存
E:Exclusive, 独占。数据只在本CPU中缓存,且没有修改,即与内存中一致
S:Shared,数据在多个CPU都有缓存,且与内存一致
I:Invalid,本CPU中的这份缓存无效
该协议要求每个缓存行维护上面4个状态中的2个状态。CPU修改数据,这个CPU的数据状态更新M,其他CPU的数据状态更新I。
重排序
保证缓存一致性,所以在CPU的L1缓存设置store buffer\ load buffer。导致CPU执行顺序和程序不一致。
Memory Barrier
1.分割代码,阻止栅栏前后没有数据依赖性的代码进行指令重排序,保证程序一定程度有序
2.强制把store buffer/高速缓存的脏数据写回主存,缓存中相应数据失效,保证内存可见性
StoreLoad Barrier保证barrier前所有内存访问指令完成后,才执行barrier后内存访问指令
happens-before原则(重排序准则)
1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行; 对象终
volatile
采用memory barrier实现,保证可见性,禁止指令重排序,不保证原子操作。
每个volatile写操作的前后插入一个StoreStore屏障
每个volatile读操作的前后面插入一个LoadLoad屏障