我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,我们互相鼓励欢迎关注微信公众号「架构染色」交流和学习
一、背景
为了性能,编译时和运行时都会有重排序,造成指令执行顺序变了,宏观上从这3点了解重排序:
- 线程内有序:如果再本线程内观察,所有的操作都是有序的,即线程内表现为串行的语义(Within Thread As-If-Serial Semantics)。
- 线程间无序:如果再一个线程中观察另一个线程,所有的操作都是无序的,即 指指令重排序现象和工作内存与主内存同步延迟现象。
- 总会有重排序:指令重排序在任何时候都有可能发生,与是否为多线程无关,之所以在单线程下感觉没有发生重排序,是因为线程内表现为串行的语义的存在。
从编译运行视角分为两类:
- 编译期重排序:包括 编译器优化的重排序。
- 运行期重排序:包括 指令级并行的重排序,内存系统的重排序。
二、内存系统的重排序
2.1 StoreBuffer
什么是StoreBuffer,已变更的数据立即写到内存太慢,所以先写到Store Buffer。 举个例子描述一下,饭店的厨师饭做好了,厨师为了提升做菜的效率,厨师不会自己做好后直接端给你,而是放到传菜间由服务员端给你;但无论服务员什么时候端给你,都不算是做好了直接给你吃,而是delay了一会儿。
- 对Cache的写入暂时来不及处理,可以先写到Buffer,后续再处理
- Store不用等待写Cache以及维护缓存一致的延迟;也可对重叠的Store进行合并
- 读Cache时需要读StoreBuffer,避免自己的Store读不到
优点:
- 可以保证core内的指令流水线持续运行,
- 它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。
- 通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。
缺点:
- 每个处理器上的写缓冲区,仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的写操作的执行顺序,不一定与内存实际发生的写操作顺序一致!
2.2 Invalidate Queue
什么是Invalidate Queue?举例解释一下:其它CPU通知说,我缓存的数据无效了,但是我在忙别的,不想打断正在做的事情,于是提供了一个通知队列,让其它把CPU把缓存无效的通知先放到通知队列中,等我忙完了再去处理
- 本地CPU来不及处理别的CPU发来的Invalidate信息时,可以在本地做一下Buff,后续处理
- 带来的问题:远程CPU写入成功,但本地CPU还是会读到旧值
优点:
- 正在处理的事情不中断
缺点:
- 处理器对内存的读操作的执行顺序,不一定与内存实际发生的写操作顺序一致!使用已经过期的数据,而不是最新的。
在两个CPU同时运行的情况下,CPU0自身视角来说,没有重排发生,一切都那么自然,但是CPU1却看到CPU0发生了重排(reordering memory)。这就是内存系统重排序。
三、禁止内存系统的重排序
3.1 读写屏障
store buffer 和 Invalidate Queue 带来的乱序如何解决?
CPU通常提供了内存屏障指令,来解决这样的乱序问题。读屏障,清空本地的invalidate queue,保证之前的所有load都已经生效;写屏障,清空本地的store buffer,使得之前的所有store操作都生效。通俗来说就是两点:
- 写屏障:保证把更新写到内存
- 读屏障:保证从内存读取最新数据
JMM把内存屏障分为四类,其实就是读、写屏障的组合:
JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障指令,来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
3.2 java中禁止重排序的操作
- volatile关键字
- unsafe 的内存屏障方法
- synchronized锁
四、最后说一句
我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。