Poison

Java Memory Model

内存模型描述了程序的可能行为,实现可以自由地生成代码,只要程序的执行结果与内存模型预测的一致即可。这为实现者提供了执行代码转换的自由,包括重排序及删除不必要的同步。

上面是翻译的… 先看一个例子:

Thread 1 Thread 2
1: r2 = A; 3: r1 = B;
2: B = 1; 4: A = 2;

两个线程执行前 A 和 B 的初始值均为 0,当两个线程执行完后,我们可能认为程序的结果一定是 r2r1 均为 0,但是,实际上可能出现 r2 等于 2,r1 等于 1,这是为什么呢,这就要从 Java 的内存模型说起了,Java 编程语言的语义允许编译器和微处理器对程序执行优化,这些优化如果与未正确同步的代码进行交互,则可能产生看似矛盾的行为,比如上面这个例子。

编译器可以在不影响该线程的单独执行结果的情况下对线程中的指令进行重排序。假设上面例子中的程序被编译器进行重排序后,产生了如下的指令:

Thread 1 Thread 2
1: B = 1; 3: r1 = B;
2: r2 = A; 4: A = 2;

在调度时,如果执行指令的顺序为 1、3、4、2,那么执行的结果将为 r2 等于 2,r1 等于 1。

对于开发人员来说,这种行为看起来程序执行出错了,但是实际上这是因为该代码没有被正确同步导致:

  • 其中一个线程有对变量的写入行为
  • 对该变量的写入会在另一个线程中被读取
  • 写入和读取操作没有被同步进行排序

有几种机制可能产生以上的重排序行为,比如 JVM 实现中的 Just-In-Time 编译器可能重排序代码,处理器也可能重排序代码,除此之外,JVM 运行所处宿主机的内存层次结构可能使代码运行看起来被重排序了。

上面的例子是指令重排序对未正确同步程序执行结果的影响,下面再看看向前替换对未正确同步程序执行结果的影响,比如以下的程序:

Thread 1 Thread 2
1: r1 = p; 6: r6 = p;
2: r2 = r1.x; 7: r6.x = 3;
3: r3 = q;
4: r4 = r3.x;
5: r5 = r1.x;

程序运行前的初始状态为 pq 为同一对象实例且 p.x 的值为 0,一种常见的编译器优化为将 r2 读取到的值重用于 r5,因为在 Thread 1 对应的执行流程看来,r2r5 的语句中间没有对 r1.x 的重新写入,所以以上程序可能被优化为如下:

Thread 1 Thread 2
1: r1 = p; 6: r6 = p;
2: r2 = r1.x; 7: r6.x = 3;
3: r3 = q;
4: r4 = r3.x;
5: r5 = r2;

当执行指令的顺序为 1、2、3、6、7、4、5 时,从开发人员的角度来看,r2 等于 0,r4 等于 3,r5 等于 0,这就导致明明 p.xThread 2 中被更新为了 3,但是 r5 没有获取到最新的 p.x 的值,使程序执行的行为看起来出错了。

Reference

17.4. Memory Model