【问题标题】:Could Java thread.start be reordered?Java thread.start 可以重新排序吗?
【发布时间】:2016-07-17 16:43:48
【问题描述】:

考虑以下从Memory Consistency - happens-before relationship in Java借用的场景:

package happen.before;

public class HappenBeforeRelationship {

private static int counter = 0;

private static void threadPrintMessage(String msg){
    System.out.printf("[Thread %s] %s\n", Thread.currentThread().getName(), msg);
}

public static void main(String[] args) {
    threadPrintMessage("Increase counter: " + ++counter);
    Thread t = new Thread(new CounterRunnable());
    t.start();
    try {
        t.join();
    } catch (InterruptedException e) {
        threadPrintMessage("Counter is interrupted");
    }
    threadPrintMessage("Finish count: " + counter);
}

private static class CounterRunnable implements Runnable {
    @Override
    public void run() {
        threadPrintMessage("start count: " + counter);
        counter++;
        threadPrintMessage("stop count: " + counter);
    }
}

我知道 JLS 中有一条规则保证 Thread.start 在启动线程中的所有操作之前发生。

当一个语句调用 Thread.start 时,每个语句都有一个 与该语句的发生之前的关系也有 与新执行的每个语句的发生前关系 线。导致创建新代码的代码的影响 线程对新线程可见。

但它并没有声称Thread.start 之前的语句与它有发生之前的关系。

所以我想知道 Thread.start 是否可以重新排序以使程序无法获得预期的输出(计数器 = 2)?如果不是,JLS 的哪一部分指定Thread.start 不能重新排序?


另一个问题:

如果join() 放在threadPrintMessage("Finish count: " + counter); 之后会发生什么? stop count: 1 会不会被打印出来?

【问题讨论】:

  • 不管开始是否重新排序,输出不一定是 2,因为无法保证更新计数器的可见性。
  • @AndyTurner,你能回顾一下stackoverflow.com/questions/16248898/… 中的第二个答案吗?它指出JLS guarantees that calling t.start() makes the change to x visible in t.run() so y is guaranteed to be assigned 1,所以是不是一个错误的说法?
  • @chainro 这绝对是正确的。正如我所说,从上到下。在Thread.start 之前,您是单线程的,因此尊重自然顺序
  • @chainro 为没有更明确表示歉意:新线程对counter 的更新不保证对主线程可见。
  • @AndyTurner,我想知道 Thread.start() 是否具有与锁相似的语义?你知道,如果我们释放锁,所有加锁后的变量,即使是非易失性的,都可以被刷新。

标签: java multithreading concurrency atomic


【解决方案1】:

Thread.start 调用之前的动作与JLS§17.4.5 引起的新线程的开始之间存在顺序关系:

  • 如果 xy 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,然后 hb(x, y)
  • 如果 hb(x, y)hb(y, z),则 hb(x, z)

稍后在同一部分中,您的问题保证中已经提到:

  • 在线程上调用start()开始线程中的任何操作之前发生。

由于 happens-before 关系的传递性,在调用 start() 之前主线程的操作与操作之间存在 happens-before 关系在开始的线程中。同样,在启动线程的操作和主线程的join 调用的成功返回之间存在happens-before关系。

换句话说,只要您没有遇到InterruptedException,更新就会正确排序,打印结果将为2。然而,这并不意味着随着 JLS§17.4.5 的继续,这些操作永远不会重新排序:

应该注意的是,两个动作之间存在 happens-before 关系并不一定意味着它们必须在实现中以该顺序发生。如果重新排序产生与合法执行一致的结果,则不是非法的。

换句话说,happens-before 关系是一个高级概念,它允许您确定程序执行的合法结果。如果像在这个例子中一样,在程序末尾的counter 中有2 是唯一合法的结果(如上所述,假设没有发生中断),JVM 可能会尽可能多地安排代码,只要程序会产生在最后的counter 中包含2 的法律结果。

您不应该再为是否可以重新排序而烦恼。这是一个不重要的实现细节。如果没有适当的 happens-before 关系,您可能会遇到出现乱序的更新,但也可能会错过更新或创建不一致的状态,因此专注于一个实现细节是没有意义的可能会在损坏的程序中导致意外的副作用。相反,应该关注什么才是正确的程序。

【讨论】:

  • 你怎么看,是“节目顺序”?正如你所写的,这就是你陈述的顺序。这是相同的顺序,如果不存在其他线程,则将执行语句。甚至您的问题也含蓄地承认 ++counter 出现在 thread.start 之前,因为我从未说过。当我谈到程序顺序时没有提到任何具体的动作时,是你想到了这个顺序。
  • @chain ro:在那个链接部分,没有丝毫提及您声称的内容。甚至不清楚您的术语“具有依赖关系的变量”应该代表什么。哪有这回事。无论如何,如果你认为,你现在比其他人都好,你为什么要在 Stackoverflow 上提出问题?
  • @chain ro:您所引用的内容仅涉及单个变量。我不明白,您如何从中得出关于“具有依赖关系的变量”之类的语句。
  • @chain ro:我的回答主要集中在你最初关于start() 和前面操作的问题上,而Andy Turner 谈到了新线程的动作和程序的结束。因为有一个join(),所以更新应该是可见的,除非出现InterruptedException(这不应该在这个简单的程序中)。我已经在回答中提到了这种可能性。我给他发了一条评论,询问他的说法是否还有其他原因。
  • 当然,如果你在counter访问之后移动join,就不再保证值了。你不应该以这种方式改变问题。我的回答末尾解释了其他问答可能读作矛盾的原因:谈论重新排序具有误导性。唯一相关的是保证了部分顺序(happens-before 关系链)。
猜你喜欢
  • 1970-01-01
  • 2012-11-12
  • 2011-02-24
  • 1970-01-01
  • 2015-02-22
  • 1970-01-01
  • 2021-01-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多