CPU乱序问题:
在CPU中,有着几层结构,每层结构的读写速度差异很大,CPU为了提高性能,可能会对原来的指令进行重新排序,具体可以参考文章:
https://blog.csdn.net/weixin_44943485/article/details/105794986
读乱序:如果一个cpu在读取数据的时候缓存不能命中,那么必须要到主存中取,在cpu到主存,再从主存返回cpu的这段时间里,相对于cpu来说,可以执行上百条指令了,如果cpu空闲等待,那么就会降低性能,所以cpu会在后面执行的指令中挑选与之前指令没有依赖关系的指令进行执行。
如何判断是否有依赖关系:具体就是前面的结果会不会影响到后面的结果
写乱序:同样的,当cpu执行写操作的时候,如果它在缓存中不能命中,那么它就必须到主存中进行操作,L2高速缓存的速度大约比cpu慢20-30倍,再到后面就更加慢了。所以cpu会进行合并写操作,在L1中查询数据时,如果缓存没能命中,那么cpu会使用另外一个缓冲区(合并写存储缓冲区),在L2尚未结束操作的时候,cpu会把待写入的数据一并放入合并写存储缓冲区中,该缓冲区大小一般是64字节。这个缓冲区允许cpu在写入或者读取该缓冲区数据的同时继续执行其他指令。
如何保证特定情况下不发生乱序
硬件内存屏障
三种方式:
- sfence:在sfence指令前的写操作必须在sfence指令后的写操作前完成
- Ifence:在Ifence指令前的读操作必须在Ifence指令后的读操作前完成
- mfence:在mfence指令前的读写操作必须在mfence指令后的读写操作前完成
原子指令,如x86上的”lock …“指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU
JVM级别如何规范
-
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底层实现
- 字节码层面
- 通过一个ACC_VOLATILE进行标识
- JVM层面 volatile内存区的读写都加屏障
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
从代码可以看出,第一个屏障防止的上面的写操作与volatile 对应的写操作重排序,下面的屏障防止了volatile 对应的写操作与下面的读操作重排序
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
从代码可以看出,第一个屏障防止的上面的读操作与volatile 对应的读操作重排序,下面的屏障防止了volatile 对应的读操作与下面的写操作重排序
- 操作系统和硬件层面
- 加入volatile关键字后,会多出一个lock前缀的指令,lock就是一个原子操作(原子操作指的是不会被线程调度机制打断的操作),当使用lock指令之后,cpu会宣告一个LOCK#信号,从而确保了多线程下互斥使用该内存地址。lock前缀指令相当于一个内存屏障,防止乱序。
synchronized底层实现
-
字节码层面
- 添加了一个
ACC_SYNCHRONIZED
的flags,同步代码块前插入monitorenter
,同步代码块结束后插入monitorexit
- 添加了一个
-
JVM层面
- 与操作系统的同步机制有关
-
OS和硬件层面
- 如果是X86,它会执行lock cmpxchg / xxx命令