当条件由其他线程设置时,为什么Java中的空时不会中断?

时间:2016-07-28 12:09:02

标签: java multithreading

在尝试对线程类进行单元测试时,我决定使用active wait来控制测试类的行为。对此使用空的while语句无法按照我的意图执行操作。所以我的问题是:

为什么第一个代码没有完成,第二个代码呢?

有一个similar question,但它没有真正的答案,也没有MCVE,而且更具体。

没有完成:

public class ThreadWhileTesting {

    private static boolean wait = true;

    private static final Runnable runnable = () -> {
        try {Thread.sleep(50);} catch (InterruptedException ignored) {}
        wait = false;
    };

    public static void main(String[] args) {
        wait = true;
        new Thread(runnable).start();
        while (wait); // THIS LINE IS IMPORTANT
    }
}

完成:

public class ThreadWhileTesting {

    private static boolean wait = true;

    private static final Runnable runnable = () -> {
        try {Thread.sleep(50);} catch (InterruptedException ignored) {}
        wait = false;
    };

    public static void main(String[] args) {
        wait = true;
        new Thread(runnable).start();
        while (wait) {
            System.out.println(wait); // THIS LINE IS IMPORTANT
        }
    }
}

我怀疑Java编译器会优化空白,但我不确定。如果打算这样做,我怎样才能达到我的目的? (是的,主动等待是有意的,因为我无法使用locks进行此测试。)

2 个答案:

答案 0 :(得分:8)

wait不是易变的,循环体是空的,所以线程没有理由相信它会改变。这是JIT' d

if (wait) while (true);

如果wait最初为真,则永远不会完成。

简单的解决方案只是制作wait volatile,这会阻止JIT进行此优化。

至于第二个版本的工作原理:System.out.println在内部同步;如JSR133 FAQ

中所述
  

在我们进入同步块之前,我们获取了监视器,它具有使本地处理器缓存无效的效果,以便从主存储器重新加载变量。

所以wait变量将在下一次循环中从主存中重新读取。

但是,您实际上并不保证另一个线程中wait变量的 write 已提交给主内存;因此,正如@assylias上面所说,它可能不适用于所有条件。 (使变量volatile也修复此问题。)

答案 1 :(得分:1)

简短的回答是这些示例的两个都不正确,但第二个是因为System.out流的实现工件而起作用。

更深层次的解释是,根据JLS内存模型,这两个示例有许多合法的执行跟踪,它们会给您带来意想不到的行为。 JLS解释如下(JLS 17.4):

  

在给定程序和该程序的执行跟踪的情况下,存储器模型描述执行跟踪是否是程序的合法执行。 Java编程语言内存模型的工作原理是检查执行跟踪中的每个读取,并根据某些规则检查该读取所观察到的写入是否有效。

     

内存模型描述了程序的可能行为。一个实现可以自由地生成它喜欢的任何代码,只要程序的所有结果执行产生一个可以由内存模型预测的结果。

     

这为实现者提供了大量的自由来执行无数的代码转换,包括重新排序操作和删除不必要的同步。

在你的第一个例子中,你有一个线程更新一个变量,第二个线程更新它,在线程之间没有任何形式的同步。要缩短(非常)冗长的故事,这意味着JLS不保证写入线程所做的内存更新对读取线程都是可见的。实际上,我上面引用的JLS文本意味着编译器有权假设变量永远不会改变。如果使用JLS 17.4中规定的规则执行分析,那么读取线程永远不会看到更改的执行跟踪是合法的。

在第二个示例中,println()调用(可能)导致一些偶然的内存缓存刷新。结果是您获得了不同的(但同样合法的)执行跟踪,并且代码"起作用"。

使示例兼容的简单修复是将wait标记声明为volatile。这意味着在一个线程中写入变量与另一个线程中的后续读取之间存在发生之前的关系。这反过来意味着在所有合法执行跟踪中,写入的结果将对readin线程可见。

这是JLS实际所说内容的简化版本。如果您真的想了解技术细节,那么它们都在规范中。但要做好一些努力工作,了解细节。