在 Java Concurrency In Practice 一书中,我们多次被告知,程序的指令可以由编译器、运行时的 JVM 甚至处理器重新排序。所以我们应该假设执行的程序不会以与我们在源代码中指定的顺序完全相同的顺序执行其指令。
但是,讨论 Java 内存模型的最后一章提供了一系列发生前发生的规则,指示 JVM 保留了哪些指令顺序。这些规则中的第一个是:
- “程序顺序规则。线程中的每个操作都发生在该线程中程序顺序后面的每个操作之前。”
我相信“程序顺序”是指源代码。
我的问题:假设这条规则,我想知道实际上可以重新排序什么指令。
“动作”定义如下:
Java 内存模型是根据操作指定的,包括对变量的读取和写入、监视器的锁定和解锁以及启动和加入线程。JMM 定义了一个偏序,称为发生在程序中的所有操作之前。为了保证执行action B的线程可以看到action A的结果(无论A和B是否发生在不同的线程中),A和B之间必须有happens before关系。在没有ahappens before ordering的情况下,两个之间的排序操作时,JVM 可以随意对它们进行重新排序。
其他提到的订单规则是:
- 监控锁定规则。监视器锁上的解锁发生在同一监视器锁上的每个后续锁之前。
- 易变的变量规则。对 volatile 字段的写入发生在对同一字段的每次后续读取之前。
- 线程开始规则。对线程的 Thread.start 调用发生在已启动线程中的每个操作之前。
- 线程终止规则。线程中的任何操作发生在任何其他线程检测到该线程已终止之前,要么通过从 Thread.join 成功返回,要么通过 Thread.isAlive 返回 false。
- 中断规则。一个线程在另一个线程上调用中断发生在被中断的线程检测到中断之前(通过抛出 InterruptedException,或者调用 isInterrupted 或中断)。
- 终结器规则。对象的构造函数的结束发生在该对象的终结器开始之前。
- 传递性。如果 A 发生在 B 之前,B 发生在 C 之前,那么 A 发生在 C 之前。
程序顺序规则的关键点是:在一个线程中。
想象一下这个简单的程序(所有变量最初都是 0):
T1:
x = 5; y = 6;
T2:
if (y == 6) System.out.println(x);
从 T1 的角度来看,执行必须与在 x (程序顺序)之后分配的 y 一致。然而,从 T2 的角度来看,情况并非如此,T2 可能会打印 0。
T1 实际上允许先分配 y,因为这 2 个分配是独立的,交换它们不会影响 T1 的执行。
通过适当的同步,T2 将始终打印 5 或什么也不打印。
编辑
您似乎误解了程序顺序的含义。节目顺序规则归结为:
happens-before在 JMM 中具有非常特殊的含义。特别是,从挂钟的角度来看,这并不意味着必须在T1之后
y=6
。x=5
这只意味着T1执行的动作顺序必须与该顺序一致。您也可以参考JLS 17.4.5:在我上面给出的示例中,您会同意,从 T1 的角度来看(即在单线程程序中),
x=5;y=6;
这是一致的,y=6;x=5;
因为您不读取这些值。在 T1 中保证下一行的语句可以查看这两个操作,无论它们执行的顺序如何。