它可能有助于解释为什么首先存在这样的规则。
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中,处理绝对时间并处理相对时间。这就是为什么nanoTime的Javadoc指出,“这种方法只能用于测量经过的时间,与任何其他系统或挂钟时间的概念无关”。System.currentTime()
System.nanoTime()
在实践中,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简短”。但我对这个领域了解不多,所以你可能想问一个单独的问题。
*:在实际实现中,这种差异取决于平台。