多线程之指令重排序

1、首先为何要指令重排序(instruction reordering)?

编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。

也就是说,对于下面两条语句:
int a = 10;
int b = 20;

在计算机执行上面两句话的时候, 有可能第二条语句会先于第一条语句执行。所以,千万 不要随意假设指令执行的顺序。


2、是不是所有的语句的执行顺序都可以重排呢?

答案是否定的。为了讲清楚这个问题,先讲解另一个概念: 数据依赖性

2.1、什么是数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖。数据依赖分下列三种类型:

名称代码示例说明
写后读a = 1;b = a;写一个变量之后,再读这个位置。
写后写a = 1;a = 2;写一个变量之后,再写这个变量。
读后写a = b;b = 1;读一个变量之后,再写这个变量。

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。所以,编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。也就是说:在单线程环境下,指令执行的最终效果应当与其在顺序执行下的效果一致,否则这种优化便会失去意义。这句话有个专业术语叫做as-if-serial semantics (as-if-serial语义)

3、重排序对多线程的影响

现在让我们来看看,重排序是否会改变多线程程序的执行结果。请看下面的示例代码:

[java]  view plain  copy
  1. class ReorderExample {  
  2.     int a = 0;  
  3.     boolean flag = false;  
  4.   
  5.     public void writer() {  
  6.         a = 1;          // 1  
  7.         flag = true;    // 2  
  8.     }  
  9.   
  10.     public void reader() {  
  11.         if (flag) {            // 3  
  12.             int i = a * a; // 4  
  13.         }  
  14.     }  
  15. }  

flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入?

答案是:不一定能看到。

由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。让我们先来看看,当操作1和操作2重排序时,可能会产生什么效果?请看下面的程序执行时序图:


上图的执行顺序是:2 -> 3 -> 4 -> 1 (这是完全存在并且合理的一种顺序,如果你不能理解,请先了解CPU是如何对多个线程进行时间分配的)

如上图所示,操作1和操作2做了重排序。程序执行时,线程A首先写标记变量flag,随后线程B读这个变量。由于条件判断为真,线程B将读取变量a。此时,变量a还根本没有被线程A写入,在这里多线程程序的语义被重排序破坏了!

下面再让我们看看,当操作3和操作4重排序时会产生什么效果。下面是操作3和操作4重排序后,程序的执行时序图:


在程序中,操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i中。

从图中我们可以看出,猜测执行实质上对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义!

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

参考:

http://www.infoq.com/cn/articles/Java-memory-model-2
http://blog.hesey.NET/2011/07/reordering.html

<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、付费专栏及课程。

余额充值