运行期重排序:内存系统的重排序

文章介绍了计算机为了性能进行的重排序现象,包括线程内有序和线程间无序的概念。讲解了内存系统的StoreBuffer和InvalidateQueue导致的重排序问题,以及它们的优缺点。为解决这些问题,提出了内存屏障作为解决方案,并提及Java中如volatile和synchronized关键字如何防止重排序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、背景

为了性能,编译时和运行时都会有重排序,造成指令执行顺序变了,宏观上从这3点了解重排序:

  1. 线程内有序:如果再本线程内观察,所有的操作都是有序的,即线程内表现为串行的语义(Within Thread As-If-Serial Semantics)。
  2. 线程间无序:如果再一个线程中观察另一个线程,所有的操作都是无序的,即 指指令重排序现象和工作内存与主内存同步延迟现象。
  3. 总会有重排序:指令重排序在任何时候都有可能发生,与是否为多线程无关,之所以在单线程下感觉没有发生重排序,是因为线程内表现为串行的语义的存在。

从编译运行视角分为两类:

  1. 编译期重排序:包括 编译器优化的重排序。
  2. 运行期重排序:包括 指令级并行的重排序,内存系统的重排序。

二、内存系统的重排序

2.1 StoreBuffer

什么是StoreBuffer,已变更的数据立即写到内存太慢,所以先写到Store Buffer。 举个例子描述一下,饭店的厨师饭做好了,厨师为了提升做菜的效率,厨师不会自己做好后直接端给你,而是放到传菜间由服务员端给你;但无论服务员什么时候端给你,都不算是做好了直接给你吃,而是delay了一会儿。

  • 对Cache的写入暂时来不及处理,可以先写到Buffer,后续再处理
  • Store不用等待写Cache以及维护缓存一致的延迟;也可对重叠的Store进行合并
  • 读Cache时需要读StoreBuffer,避免自己的Store读不到
  1. 优点:

    • 可以保证core内的指令流水线持续运行,
    • 它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。
    • 通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。
  2. 缺点:

    • 每个处理器上的写缓冲区,仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的写操作的执行顺序,不一定与内存实际发生的写操作顺序一致!

2.2 Invalidate Queue

什么是Invalidate Queue?举例解释一下:其它CPU通知说,我缓存的数据无效了,但是我在忙别的,不想打断正在做的事情,于是提供了一个通知队列,让其它把CPU把缓存无效的通知先放到通知队列中,等我忙完了再去处理

  • 本地CPU来不及处理别的CPU发来的Invalidate信息时,可以在本地做一下Buff,后续处理
  • 带来的问题:远程CPU写入成功,但本地CPU还是会读到旧值
  1. 优点:
    • 正在处理的事情不中断
  2. 缺点:
    • 处理器对内存的读操作的执行顺序,不一定与内存实际发生的写操作顺序一致!使用已经过期的数据,而不是最新的。

在两个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锁

四、最后说一句

我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。

<think>嗯,用户让我介绍Java多线程中的重排序现象,并提供一个demo。好的,首先我需要回忆一下什么是重排序重排序Java内存模型中的概念,指的是编译器和处理器为了优化性能,可能会重新排序指令的执行顺序。这在单线程环境下没问题,但在多线程中可能导致可见性问题或者意外的结果。 接下来,用户可能需要一个具体的例子来理解这个概念。我应该设计一个简单的例子,展示在多线程环境下由于重排序导致的问题。记得要说明重排序可能引发的现象,比如某个线程看到的变量值不符合预期。 首先,考虑使用两个变量,比如a和flag。一个线程写入a和flag,另一个线程读取它们。如果发生重排序,写入线程可能会先写flag再写a,这时候读取线程可能在看到flag为true时,a仍然是0,导致输出不符合预期的情况。 不过,我需要确保这个例子确实能触发重排序现象。可能需要在没有同步机制的情况下运行,比如不使用volatile或synchronized。同时,可能需要让程序循环多次,因为重排序并非每次都发生,这样可以增加观察到现象的概率。 然后,编写代码的时候,要创建两个线程,一个执行写操作,另一个执行读操作。写线程先设置a=1,再设置flag=true。而读线程在循环中等待flag变为true,然后打印a的值。如果发生重排序,写线程可能先执行flag=true,之后a=1还没执行,这时候读线程看到flag为true,但a仍然是0,就会输出a=0的情况。 不过,实际运行中可能很难观察到这种情况,因为现代处理器的内存模型和JIT优化可能不同。可能需要多次运行或者在特定的环境下才能出现。所以应该在demo中让程序循环多次,并统计出现异常情况的次数。 另外,可能需要加入一些注释,解释代码的意图,以及可能的执行顺序。同时,要提醒用户,这个现象不是每次都能复现,依赖于JVM和硬件环境。 最后,还要给出解决方案,比如使用volatile关键字来禁止重排序,或者使用同步块。这样可以对比有同步和无同步情况下的不同结果,帮助用户理解如何避免重排序的问题。 总结一下,demo的结构应该是:两个线程,共享变量a和flag,无同步机制,循环多次触发,统计出现a=0的次数。然后解释原因,并提供解决方案。</think>下面通过一个简单的示例代码来演示多线程环境下的指令重排序现象: ### 现象说明 在缺乏同步机制的情况下,编译器和处理器可能对指令进行重排序,导致多线程程序出现反直觉的执行结果。 ### 示例代码 ```java public class ReorderingDemo { private int a = 0; private boolean flag = false; // 写线程执行 public void writer() { a = 1; // 操作1 flag = true; // 操作2 } // 读线程执行 public void reader() { if (flag) { // 操作3 System.out.println("a = " + a); // 操作4 } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10000; i++) { ReorderingDemo demo = new ReorderingDemo(); Thread writer = new Thread(demo::writer); Thread reader = new Thread(demo::reader); writer.start(); reader.start(); writer.join(); reader.join(); } } } ``` ### 可能输出结果 ``` a = 0 // 出现这种结果即表明发生了重排序 a = 1 a = 1 a = 0 // 再次出现非预期结果 ... ``` ### 现象解析 1. **正常情况**(无重排序): - 写线程先执行 `a = 1`,再执行 `flag = true` - 读线程看到 `flag == true` 时,`a` 必然已经是 1 2. **重排序发生时**: - 写线程可能先执行 `flag = true`,再执行 `a = 1` - 读线程可能在 `a` 未被赋值时读取到 `a = 0` ### 原因分析 - 编译器/处理器可能交换操作1和操作2的执行顺序 - 内存可见性问题导致读线程无法立即看到修改 --- ### 解决方案 使用 `volatile` 关键字禁止重排序: ```java private volatile boolean flag = false; ``` ### 修正后的线程安全代码 ```java public void writer() { a = 1; // 写操作在volatile写之前发生 flag = true; // volatile写,禁止重排序 } public void reader() { if (flag) { // volatile读,保证可见性 System.out.println(a); // 读操作在volatile读之后发生 } } ``` --- ### 关键点总结 1. 重排序可能发生在编译期(JIT优化)或运行期(CPU指令重排) 2. 未同步的多线程程序可能观察到违背代码顺序的执行结果 3. `volatile` 通过内存屏障(Memory Barrier)禁止特定类型的重排序 4. 最终一致性需要通过正确的同步机制来保证 建议在实际开发中始终使用正确的同步机制(如 `synchronized`、`volatile` 或并发工具类)来避免这类问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值