有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

多线程为什么Java中的空while在其他线程设置条件时不会中断?

在尝试对线程类进行单元测试时,我决定使用活动等待来控制被测试类的行为。为此使用空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
        }
    }
}

我怀疑空while会被Java编译器优化,但我不确定。如果这种行为是有意的,我如何才能实现我想要的?(是的,由于我无法将locks用于此测试,因此打算进行活动等待。)


共 (2) 个答案

  1. # 1 楼答案

    简而言之,这些示例中的都是不正确的,但第二个示例之所以有效,是因为System.out流的实现工件

    更深入的解释是,根据JLS记忆模型,这两个例子有许多法律执行痕迹,这些痕迹会给(你)带来意想不到的行为。JLS是这样解释的(JLS 17.4):

    A memory model describes, given a program and an execution trace of that program, whether the execution trace is a legal execution of the program. The Java programming language memory model works by examining each read in an execution trace and checking that the write observed by that read is valid according to certain rules.

    The memory model describes possible behaviors of a program. An implementation is free to produce any code it likes, as long as all resulting executions of a program produce a result that can be predicted by the memory model.

    This provides a great deal of freedom for the implementor to perform a myriad of code transformations, including the reordering of actions and removal of unnecessary synchronization.

    在第一个示例中,一个线程更新变量,另一个线程更新变量,而tro线程之间没有任何形式的同步。简而言之,这意味着JLS不能保证写入线程进行的内存更新对读取线程都是可见的。实际上,我上面引用的JLS文本意味着编译器有权假设变量从未更改。如果使用JLS 17.4中规定的规则执行分析,则读取线程从未看到更改的执行跟踪是合法的

    在第二个示例中,println()调用(可能)会导致内存缓存意外刷新。结果是,您得到了一个不同的(但同样合法的)执行跟踪,代码“工作”

    使两个示例都起作用的简单修复方法是将wait标志声明为volatile。这意味着在一个线程中写入变量和在另一个线程中后续读取之间存在之前发生的关系。这反过来意味着,在所有合法执行跟踪中,readin线程都可以看到写入的结果

    这是JLS实际表述的一个大大简化的版本。如果你真的想了解技术细节,它们都在规范中。但是要做好准备,努力了解细节

  2. # 2 楼答案

    wait不是易变的,循环体是空的,因此线程没有理由相信它会改变。这是准时到的

    if (wait) while (true);
    

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

    简单的解决方案就是使wait{},这会阻止JIT进行这种优化


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

    Before we can enter a synchronized block, we acquire the monitor, which has the effect of invalidating the local processor cache so that variables will be reloaded from main memory.

    因此wait变量将在下一次循环中从主内存中重新读取

    然而,实际上并不能保证另一个线程中wait变量的写入被提交到主存;因此,正如@assylias在上面指出的,它可能不会在所有条件下都起作用。(使变量volatile也可以解决这个问题)