Java之volatile如何保证可见性和指令重排序
1 我们先了解CPU缓存
CPU缓存为了解决CPU运算速度与内存读写速度不匹配的问题,因为CPU运算速度要比内存读写速度快得多
- 一次主内存的访问通常在几十到几百个时钟周期
- 一次L1高速缓存的读写只需要1~2个时钟周期
- 一次L2高速缓存的读写也只需要数十个时钟周期
CPU大多数情况下读写都不会直接访问内存,取而代之的是CPU缓存,CPU缓存是位于CPU与内存之间的临时存储器(简单理解为寄存器),它容量比内存小得多但是交换速度却比内存快得多。而缓存中的数据是内存中的一小部分数据,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先从缓存中读取,从而加快读取速度
CPU缓存可分为:一级缓存(是与CPU结合最为紧密的CPU缓存)、二级缓存、三级缓存,每一级缓存中所存储的数据全部都是下一级缓存中的一部分
当CPU要读取数据时,首先从一级缓存中查找,如果没有再从二级缓存中查找,如果还是没有再从三级缓存中或内存中查找。一般来说每级缓存的命中率大概都有80%左右,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取。
CPU执行计算的过程如下:
- 程序以及数据被加载到主内存
- 指令和数据被加载到CPU缓存
- CPU执行指令,把结果写到高速缓存
- 高速缓存中的数据写回主内存
2 总线锁
每个CPU都有一级缓存,但是,我们却无法保证每个CPU的一级缓存数据都是一样的,如何保证各个CPU缓存中的数据是一致的。就是CPU的缓存一致性问题
1)总线锁
一种处理一致性问题的办法是使用Bus Locking(总线锁)。当一个CPU对其缓存中的数据进行操作的时候,往总线中发送一个Lock信号。 这个时候,所有CPU收到这个信号之后就不操作自己缓存中的对应数据了,也就是把数据直接写入主内存,当操作结束,释放锁以后,所有的CPU就去内存中获取最新数据更新。
3 volatile如何保证可见性
我们把有volatile修饰的变量编译成部分汇编,这里有个lock指令
0x01a3de24: lock addl $0X0,(%esp);
如果是写操作,cpu会发出一个lock指令,CUP会把数据直接写到到主内存
如果是读操作,cpu会发出一个unlock指令, 所有的CPU就去内存中获取最新数据更新
4 volatile如何保证指令重排序
现代的操作系统都是多处理器.而每一个处理器都有自己的缓存,并且这些缓存并不是实时都与内存发生信息交换.这样就可能出现一个cpu上的缓存数据与另一个cpu上的缓存数据不一致的问题.而这样在多线程开发中,就有可能导致出现一些异常行为.
而操作系统底层为了这些问题,提供了一些内存屏障用以解决这样的问题.目前有4种屏障.
- 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写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
由于内存屏障的作用,避免了volatile变量和其它指令重排序
参考链接:
https://crowhawk.github.io/2018/02/10/volatile/
https://www.jianshu.com/p/ef8de88b1343
https://my.oschina.net/LucasZhu/blog/1537330