解释Java并发中的"程序顺序规则"

Abh*_*k S 13 java concurrency java-memory-model

程序顺序规则指出"线程中的每个操作都发生在 - 在程序顺序中稍后出现的该线程中的每个操作之前"

1.I读取另一个线程,一个动作

  • 读取和写入变量
  • 锁定和解锁显示器
  • 开始和加入线程

这是否意味着可以按顺序更改读取和写入,但读取和写入不能使用在第2行或第3行中指定的操作更改顺序?

2."程序顺序"是什么意思?

对示例的解释将非常有用.

其他相关问题

假设我有以下代码:

long tick = System.nanoTime(); //Line1: Note the time
//Block1: some code whose time I wish to measure goes here
long tock = System.nanoTime(); //Line2: Note the time
Run Code Online (Sandbox Code Playgroud)

首先,它是一个单线程应用程序,以保持简单.编译器注意到它需要检查两次时间并且还注意到与周围时间注释行没有依赖关系的代码块,因此它看到了重新组织代码的可能性,这可能导致Block1不被定时调用所包围在实际执行期间(例如,考虑此顺序Line1-> Line2-> Block1).但是,作为程序员,我可以看到Line1,2和Block1之间的依赖关系.Line1应紧接在Block1之前,Block1需要有限的时间才能完成,并且Line2立即成功.

所以我的问题是:我是否正确测量了块?

  • 如果是,那么是什么阻止了编译器重新排列顺序.
  • 如果不是,(通过Enno的回答后认为是正确的)我该怎么做才能防止它.

PS:我最近在SO中提出的另一个问题偷了这个代码.

Enn*_*oji 21

它可能有助于解释为什么这样的规则首先存在.

Java是一种过程语言.即你告诉Java如何为你做点什么.如果Java执行的指令不是您编写的顺序,那么它显然不起作用.例如,在下面的例子中,如果Java会做2 - > 1 - > 3那么炖菜就会被破坏.

1. Take lid off
2. Pour salt in
3. Cook for 3 hours
Run Code Online (Sandbox Code Playgroud)

那么,为什么规则不是简单地说"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
Run Code Online (Sandbox Code Playgroud)

如果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);
Run Code Online (Sandbox Code Playgroud)

如果编译器可以证明所compute()占用的数据少于我们允许的系统时钟的最大偏差,那么按以下方式重新排序是合法的:

long tick = System.currentTimeMillis();
long tock = System.currentTimeMillis();
result = compute();
print(result + ":" + tick - tock);
Run Code Online (Sandbox Code Playgroud)

由于这样做不会违反我们定义的规范,因此不会违反语义.

您还问为什么这不包含在JLS中.我认为答案是"保持JLS简短".但是我对这个领域知之甚少,所以你可能想问一个单独的问题.

*:在实际实现中,这种差异取决于平台.

  • @AndrewBissell:哦和btw for currentTimeMillis,计算可能会受到操作系统对时钟校正的影响(即当操作系统恰好在该时间间隔内进行校正时,tock - tick可能会变为负值).所以,即使没有任何重新排序相关的东西,你也会遇到这个问题. (2认同)

And*_*ell 7

程序顺序规则保证在单个线程内,编译器引入的重新排序优化不会产生与程序以串行方式执行时发生的结果不同的结果.如果没有同步的那些线程观察到其状态,则无法保证线程的操作在任何其他线程中可能出现的顺序.

请注意,此规则仅说明程序的最终结果,而不是该程序中单个执行的顺序.例如,如果我们有一个方法对某些局部变量进行以下更改:

x = 1;
z = z + 1;
y = 1;
Run Code Online (Sandbox Code Playgroud)

编译器可以自由地重新排序这些操作,但它认为最适合提高性能.想到这一点的一种方法是:如果你可以在源代码中重新排序这些操作并仍然获得相同的结果,编译器可以自由地做同样的事情.(事实上​​,它可以更进一步完全丢弃显示没有结果的操作,例如调用空方法.)

使用第二个项目符号点,监视器锁定规则开始起作用:"监视器上的解锁发生 - 在主监视器锁定的每个后续锁定之前." (Java Concurrency in Practice p.341)这意味着获取给定锁的线程将具有在释放该锁之前在其他线程中发生的操作的一致视图.但是,请注意,此保证仅适用于在两个不同的线程releaseacquire相同的锁.如果线程A在释放Lock X之前执行了大量的操作,然后线程B获取了锁定Y,则线程B不能保证具有A的前X操作的一致视图.

这是可能的读取和写入变量与重新排序startjoin 如果.)这样做不会打破内线程程序顺序,湾)的变量还没有其他"之前发生"线程同步语义应用他们,比如把它们存放在volatile田里.

一个简单的例子:

class ThreadStarter {
   Object a = null;
   Object b = null;
   Thread thread;

   ThreadStarter(Thread threadToStart) {
      this.thread = threadToStart;
   }

   public void aMethod() {
      a = new BeforeStartObject();
      b = new BeforeStartObject();
      thread.start();
      a = new AfterStartObject();
      b = new AfterStartObject();

      a.doSomeStuff();
      b.doSomeStuff();
   }
}
Run Code Online (Sandbox Code Playgroud)

由于字段ab方法aMethod()没有以任何方式同步,并且启动thread操作不会更改字段写入的结果(或者使用这些字段执行操作),编译器可以自由地重新排序thread.start()到任何位置方法.它唯一不能用的顺序是在写入一个字段之后移动将其中一个s写入字段aMethod()的顺序,或者BeforeStartObject在写入字段之前AfterStartObject移动一个doSomeStuff()字段上的一个调用AfterStartObject.(也就是说,假设这种重新排序会doSomeStuff()以某种方式改变调用的结果.)

这里要记住的关键是,在没有同步的情况下,启动的线程aMethod()理论上可以观察其中一个或两个字段a以及b它们在执行aMethod()(包括null)期间所处的任何状态.

补充问题的答案

如果要在任何测量中实际使用它们,则对于代码的分配ticktock不能重新排序Block1,例如通过计算它们之间的差异并将结果打印为输出.这样的重新排序显然会破坏Java的线程内部as-if-serial语义.它改变了通过执行指定程序顺序中的指令所获得的结果.如果分配用于任何测量并且对程序结果没有任何副作用,则它们可能会被编译器优化为无操作而不是重新排序.