这可能有助于解释为什么首先存在这样的规则。
Java 是一种过程语言。即你告诉Java 如何为你做某事。如果 Java 不按照您编写的顺序执行您的指令,它显然不会工作。例如,在下面的示例中,如果 Java 会执行 2 -> 1 -> 3,那么炖菜就会被毁掉。
1. Take lid off
2. Pour salt in
3. Cook for 3 hours
那么,为什么规则不简单地说“Java 按照你写的顺序执行你写的东西”?简而言之,因为 Java 很聪明。举个例子:
1. Take eggs out of the freezer
2. Take lid off
3. Take milk out of the freezer
4. Pour egg and milk in
5. Cook for 3 hours
如果 Java 像我一样,它只会按顺序执行。然而,Java 足够聪明,可以理解它更有效,并且最终结果将是相同的,如果它执行 1 -> 3 -> 2 -> 4 -> 5 (您不必再次走到冰箱,并且这不会改变配方)。
因此,规则“线程中的每个动作都发生-在该线程中的每个动作之前在程序顺序后面出现”试图说的是,“在单个线程中,您的程序将运行,就好像它是在确切的你写的顺序。我们可能会改变幕后的顺序,但我们确保这些都不会改变输出。
到目前为止,一切都很好。为什么它不跨多个线程做同样的事情?在多线程编程中,Java 不够聪明,无法自动完成。它将用于某些操作(例如加入线程、启动线程、何时使用锁(监视器)等),但对于其他内容,您需要明确告诉它不要进行会改变程序输出的重新排序(例如volatile
字段上的标记,使用锁等)。
注意:
关于“发生在关系之前”的快速附录。这是一种奇特的说法,无论 Java 可能做什么重新排序,东西 A 都会发生在东西 B 之前。在我们后面奇怪的炖菜示例中,“第 1 步和第 3 步发生在第 4 步“倒入鸡蛋和牛奶”之前。还例如,“第 1 步和第 3 步不需要发生之前的关系,因为它们不以任何方式相互依赖”
关于附加问题和对评论的回应
首先,让我们确定“时间”在编程世界中的含义。在编程中,我们有“绝对时间”的概念(现在世界上的时间是多少?)和“相对时间”的概念(距离 x 已经过去了多少时间?)。在一个理想的世界里,时间就是时间,但除非我们有一个内置的原子钟,否则绝对时间必须不时地修正。另一方面,对于相对时间,我们不想要更正,因为我们只对事件之间的差异感兴趣。
在 Java 中,System.currentTime()
处理绝对时间和System.nanoTime()
处理相对时间。这就是为什么 nanoTime 的 Javadoc 指出,“此方法只能用于测量经过的时间,与任何其他系统或挂钟时间概念无关”。
实际上,currentTimeMillis 和 nanoTime 都是本机调用,因此编译器实际上无法证明重新排序是否不会影响正确性,这意味着它不会重新排序执行。
但是让我们想象一下,我们想要编写一个编译器实现,它实际上会查看本机代码并重新排序所有内容,只要它是合法的。当我们查看 JLS 时,它告诉我们的只是“只要无法检测到任何东西,您都可以重新排序”。现在作为编译器编写者,我们必须决定重新排序是否会违反语义。对于相对时间(nanoTime),如果我们重新排序执行,它显然是无用的(即违反语义)。现在,如果我们重新排序绝对时间(currentTimeMillis)会违反语义吗?只要我们可以限制从世界时间来源(比如说系统时钟)到我们决定的任何时间(比如“50ms”)*的差异,我就说不。对于以下示例:
long tick = System.currentTimeMillis();
result = compute();
long tock = System.currentTimeMillis();
print(result + ":" + tick - tock);
如果编译器可以证明compute()
与系统时钟的最大偏差小于我们可以允许的任何最大偏差,那么将其重新排序如下是合法的:
long tick = System.currentTimeMillis();
long tock = System.currentTimeMillis();
result = compute();
print(result + ":" + tick - tock);
因为这样做不会违反我们定义的规范,因此不会违反语义。
您还问为什么这不包含在 JLS 中。我认为答案是“保持 JLS 简短”。但我对这个领域知之甚少,因此您可能想为此提出一个单独的问题。
*:在实际实现中,此差异取决于平台。