与 System.out 关联的 Java 线程的奇怪行为

Strange behavior of a Java thread associated with System.out

我有一个简单的 TestThreadClientMode class 来测试竞争条件。我尝试了两次:

  1. 当我 运行 下面的代码 System.out.println(count); 在第二个线程中注释时,输出是:

OS: Windows 8.1 flag done set true ...

并且第二个线程永远存在。因为第二个线程永远不会看到主线程设置为 true 的 done 标志的变化。

  1. 当我取消注释 System.out.println(count); 时,输出是:

    OS: Windows 8.1 0 ... 190785 190786 flag done set true Done! Thread-0 true

程序在 1 秒后停止。

System.out.println(count); 是如何让第二个线程看到 done 中的变化的?

代码

public class TestThreadClientMode {
    private static boolean done;
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            public void run() {
                int count = 0;
                while (!done) {
                    count ++;
                    //System.out.println(count);
                }
                System.out.println("Done! " + Thread.currentThread().getName() + "  " + done);
            }
        }).start();
        System.out.println("OS: " + System.getProperty("os.name"));

        Thread.sleep(1000);
        done = true;

        System.out.println("flag done set true ");
    }
}

这是 memory consistency errors 的绝妙例子。简单地说,变量被更新但第一个线程并不总是看到变量变化。这个问题可以通过声明 done 变量 volatile 来解决:

private static volatile boolean done;

在这种情况下,对变量的更改对所有线程都是可见的,程序总是在一秒后终止。

更新: 看起来使用 System.out.println 确实解决了内存一致性问题 - 这是因为打印功能使用了实现同步的底层流.同步建立了一个 happens-before 关系,如我链接的教程中所述,它与 volatile 变量具有相同的效果。 (来自 this answer 的详细信息。也感谢 @Chris K 指出了流操作的副作用。)

How did System.out.println(count); make the second thread see the change in done?

您正在目睹 println 的副作用;您的程序正在遭受并发竞争条件。在 CPU 之间协调数据时,重要的是告诉 Java 程序你想在 CPU 之间共享数据,否则 CPU 可以自由延迟相互交流。

在 Java 中有几种方法可以做到这一点。主要的两个是关键字 'volatile' 和 'synchronized',它们都将硬件人员所说的 'memory barriers' 插入到您的代码中。如果不在代码中插入 'memory barriers',则并发程序的行为未定义。也就是说,我们不知道 'done' 何时对另一个 CPU 可见,因此这是一个竞争条件。

这里是System.out.println的实现;注意同步的使用。 synchronized 关键字负责在生成的汇编器中放置内存屏障,它的副作用是使变量 'done' 对另一个 CPU.

可见
public void println(boolean x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

您的程序的正确修复是在读取完成时放置一个读取内存屏障,在写入时放置一个写入内存屏障。通常这是通过从同步块中读取或写入 'done' 来完成的。在这种情况下,将变量 done 标记为 volatile 将具有相同的净效果。您还可以对变量使用 AtomicBoolean 而不是 boolean

println() 实现包含显式内存屏障:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

这会导致调用线程刷新所有变量。

以下代码将具有与您的代码相同的行为:

    public void run() {
        int count = 0;
        while (!done) {
            count++;
            synchronized (this) {
            }
        }
        System.out.println("Done! " + Thread.currentThread().getName() + "  " + done);
    }

其实任何对象都可以用于监听,下面的也可以:

synchronized ("".intern()) {
}

另一种创建显式内存屏障的方法是使用 volatile,因此以下方法有效:

new Thread() {
    private volatile int explicitMemoryBarrier;
    public void run() {
        int count = 0;
        while (!done) {
            count++;
            explicitMemoryBarrier = 0;
        }
        System.out.println("Done! " + Thread.currentThread().getName() + "  " + done);
    }
}.start();