14

程序顺序规则规定“线程中的每个动作都发生在该线程中的每个动作之前,然后在程序顺序中出现”

1.我在另一个线程中读到一个动作

  • 读取和写入变量
  • 监视器的锁定和解锁
  • 开始和加入线程

这是否意味着读取和写入可以按顺序更改,但读取和写入不能通过第 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

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

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

  • 如果是,是什么阻止编译器重新排列顺序。
  • 如果不是,(在通过 Enno 的回答后认为是正确的)我能做些什么来防止它。

PS:我从最近在 SO 中提出的另一个问题中窃取了此代码。

4

5 回答 5

21

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

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 简短”。但我对这个领域知之甚少,因此您可能想为此提出一个单独的问题。

*:在实际实现中,此差异取决于平台。

于 2013-03-27T08:50:19.470 回答
7

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

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

x = 1;
z = z + 1;
y = 1;

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

随着您的第二个要点,监视器锁定规则开始发挥作用:“监视器上的解锁发生在该主监视器锁定上的每个后续锁定之前。” ( Java Concurrency in Practice p. 341) 这意味着获取给定锁的线程将在释放该锁之前对其他线程中发生的操作具有一致的视图。但是,请注意,此保证仅适用于两个不同的线程release或相同acquirelock。如果线程 A 在释放 Lock X 之前做了一堆事情,然后线程 B 获得了 Lock Y,则线程 B 无法保证对 A 的 pre-X 操作有一致的看法。

如果 a.) 这样做不会破坏线程内程序顺序,并且b.) 变量没有应用其他“之前发生”线程同步语义,则可以对变量的读取start和写入进行重新排序它们,比如将它们存储在字段中。joinvolatile

一个简单的例子:

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();
   }
}

由于字段ab方法aMethod()不会以任何方式同步,并且启动的thread操作不会改变写入字段的结果(或对这些字段的操作),编译器可以自由地重新排序thread.start()到任何地方方法。唯一不能对 of 的顺序做的事情是,在将 an 写入该字段之后aMethod(),将写入其中一个BeforeStartObjects的顺序移动AfterStartObject到该字段,或者在将a写入doSomeStuff()该字段之前移动对该字段的调用之一。AfterStartObject(也就是说,假设这种重新排序会doSomeStuff()以某种方式改变调用的结果。)

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

附加问题答案

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

于 2013-03-27T09:04:39.070 回答
1

在我回答问题之前,

读取和写入变量

应该

易失性读取和易失性写入(同一字段)

程序顺序不保证这发生在关系之前,而是发生在关系之前保证程序顺序

对于您的问题:

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

答案实际上取决于首先发生什么动作,然后发生什么动作。查看JSR 133 Cookbook for Compiler Writers。有一个Can Reorder网格,列出了可能发生的允许的编译器重新排序。

例如,一个Volatile Store可以在Normal Store之上或之下重新排序,但Volatile Store 不能在Volatile Load之上或之下重新排序。这一切都假设线程内语义仍然成立。

“节目顺序”是什么意思?

这是来自JLS

在每个线程 t 执行的所有线程间动作中,t 的程序顺序是一个总顺序,它反映了根据 t 的线程内语义执行这些动作的顺序。

换句话说,如果您可以更改变量的写入和加载方式,使其执行方式与您编写它的方式完全相同,那么它可以维护程序顺序。

例如

public static Object getInstance(){
    if(instance == null){
         instance = new Object();
    }
    return instance;
}

可以重新订购

public static Object getInstance(){
     Object temp = instance;
     if(instance == null){
         temp = instance = new Object();
     }
     return temp;
}
于 2013-03-27T12:16:21.797 回答
0

Java 教程http://docs.oracle.com/javase/tutorial/essential/concurrency/memconsist.html说,happens-before 关系只是保证一个特定语句写入的内存对另一个特定语句可见。这是一个插图

int x;

synchronized void x() {
    x += 1;
}

synchronized void y() {
    System.out.println(x);
}

synchronized创建一个happens-before关系,如果我们删除它,就不能保证在线程A递增x之后线程B会打印1,它可能会打印0

于 2013-03-27T08:47:06.773 回答
0

它只是意味着虽然线程可以被多路复用,但线程的动作/操作/指令的内部顺序将保持不变(相对)

线程1:T1op1,T1op2,T1op3...线程2:T2op1,T2op2,T2op3...

尽管线程之间的操作顺序(Tn'op'M)可能会有所不同,但T1op1, T1op2, T1op3线程内的操作将始终按此顺序进行,因此T2op1, T2op2, T2op3

例如:

T2op1, T1op1, T1op2, T2op2, T2op3, T1op3
于 2013-03-27T08:32:34.953 回答