“Java 重排序对多线程的影响”的版本间的差异

来自姬鸿昌的知识库
跳到导航 跳到搜索
 
第12行: 第12行:
 
     public void reader() {
 
     public void reader() {
 
         if (flag) { // 3
 
         if (flag) { // 3
             int i = a * a;
+
             int i = a * a; // 4
 
         }
 
         }
 
     }
 
     }

2023年3月2日 (四) 06:49的最新版本

public class ReorderExample {

    int a = 0;
    boolean flag = true;

    public void writer() {
        a = 1; // 1
        flag = true; // 2
    }

    public void reader() {
        if (flag) { // 3
            int i = a * a; // 4
        }
    }

}

flag 变量是个标记,用来标识变量 a 是否已被写入。

这里假设有两个线程 A 和 B,A 首先执行 writer() 方法,随后 B 线程接着执行 reader() 方法。

线程 B 在执行操作 4(int i = a * a;)时,能否看到线程 A 在操作 1(a = 1;) 对共享变量 a 的写入呢?

答案是:不一定能看到

操作 1 和 操作 2 重排序

由于操作 1 和操作 2 没有数据依赖关系,编译器和处理器可以对这两个操作重排序;

同样,操作 3 和操作 4 没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。

当操作 1 和操作 2 重排序时,可能会产生什么效果?看下图:

生成缩略图出错:无法将缩略图保存到目标地点
虚箭线标识错误的读操作


如图所示,操作 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 语义允许对存在控制依赖的操作做重排序的原因);

但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。