与 System.out 关联的 Java 线程的奇怪行为
Strange behavior of a Java thread associated with System.out
我有一个简单的 TestThreadClientMode
class 来测试竞争条件。我尝试了两次:
- 当我 运行 下面的代码
System.out.println(count);
在第二个线程中注释时,输出是:
OS: Windows 8.1
flag done set true
...
并且第二个线程永远存在。因为第二个线程永远不会看到主线程设置为 true 的 done
标志的变化。
当我取消注释 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();
我有一个简单的 TestThreadClientMode
class 来测试竞争条件。我尝试了两次:
- 当我 运行 下面的代码
System.out.println(count);
在第二个线程中注释时,输出是:
OS: Windows 8.1
flag done set true
...
并且第二个线程永远存在。因为第二个线程永远不会看到主线程设置为 true 的 done
标志的变化。
当我取消注释
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();