了解 Java volatile 字段跨线程读写的协调
Understanding of coordination for Java volatile fields' reads & writes across threads
我有以下代码:
private volatile boolean run = true;
private Object lock =new Object();
.........
Thread newThread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(Thread.currentThread().getName()
+ " run:" + run);
System.out.println(Thread.currentThread().getName()
+ " setting run to false");
run = false;
System.out.println(Thread.currentThread().getName()
+ " run:" + run);
}
}});
newThread.start();
while(true) {//no synchronization, so no coordination guarantee
System.out.println(Thread.currentThread().getName() + "* run: "+run);
if(run == false) {
System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting...");
System.exit(0);
}
}
which generates the following output:
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
Thread-0 setting run to false
Thread-0 run:false
main* run: true <- what causes this???
main** run: false
Exiting...
我试图理解为什么在主线程中发生 main* 运行: true 的异常,考虑到 run
是一个易变字段根据 Java 内存模型规范,Thread-0
中的易失性写入应该立即被 main
线程可见。我知道 Thread-0
中的同步在这里是无关紧要的,但我对 volatile 的这种行为感到困惑。我在这里错过了什么?
另一个甚至更奇怪的 运行 产生了这个:
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main** run: false
Exiting...
Thread-0 run:false
或者这种行为是可以预期的,如果是,为什么?谢谢。
编辑: 正如评论中所问,我正在用我有时但并非总是看到的预期输出更新 post:
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
Thread-0 setting run to false
main* run: true
main* run: true
main* run: true
Thread-0 run:false
main** run: false
Exiting...
也就是说,我不想看到:
main* run: true
出现在
之后
Thread-0 run:false
或
main** run: false
Exiting...
出现在
之前
Thread-0 run:false
我没有看到问题。这里的锁是没用的。 volatile 也意味着变量在自身内部是同步的。这里发生了什么。每当有多个线程时,每个线程都会自行运行而不关心其他线程。所以在这种情况下,我们有两个线程:main 和 thread-0。 Main 自行运行并到达打印变量 run
的点,因此它打印它。另一个线程休眠了一点(这应该无关紧要,也不应该是让其他线程先工作的方式),然后将变量 run
更改为 false。主线程读取新值存在
跟着时间顺序你就明白了
Thread newThread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(Thread.currentThread().getName()
+ " run:" + run);
System.out.println(Thread.currentThread().getName()
+ " setting run to false");
run = false; //<---- time_4
System.out.println(Thread.currentThread().getName()
+ " run:" + run); //<---- time_5
}
}});
newThread.start();
while(true) { //<---- time_2
System.out.println(Thread.currentThread().getName() + "* run: "+run); //<--- time_3 getting the value of run variable. //<---- time_6 printing
if(run == false) { //<---- time_1 (run == true) // <---- 2nd iteration time_7 (run == false)
System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting..."); //<---- time_8
System.exit(0);
}
}
无论如何,这里是如何修复您的代码以获得预期的输出(注意:volatile 在这里没有做任何事情):
synchronized (lock) {
if(run == false) {
System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting...");
System.exit(0);
}
}
这就是 volatile 对变量的基本作用 run
:
// run = false; //becomes ========
synchronized(someLock) {
run = flase;
}
// =======================
//System.out.println(run); //becomes =========
synchronized(someLock) {
boolean tmpBoolean = run;
}
System.out.println(tmpBoolean);
//=================
至于你程序的正确性,synchronized
块是完全没有必要的。如果您只从单个线程同步锁,那么 JIT compiler eliminates this lock altogether 很有可能。
真正让您感到困惑的是 JMM 保证 volatile
字段保证在写入后可以被其他线程看到其更新值。但是,此保证 并不 暗示写入 volatile
字段的线程会立即传递此值并停止,直到将新值传递给所有其他线程。相反,保证其他线程最终需要看到更新后的值。
这意味着如果线程 A 写入 volatile
字段,线程 B 保证:
- 终于看到这个新值了。
- 不读取之前写入可变字段的任何值及其 "old" 值。
另外,请注意调用 System.out.println
隐式同步 System.out
对象(查看 PrintWriter
代码)。考虑到您在单个监视器上同步两个线程这一事实也解释了观察到的输出。我假设字符串是在 System.out
监视器被您的字段设置线程锁定时创建的。在这种情况下,线程首先创建要写入的字符串,然后等待另一个线程释放此监视器,这就是为什么您通常会观察到带有 "old" 内容的输出。
我的意思是说
System.out.println(Thread.currentThread().getName() + "* run: " + run);
不是原子的。分两步,语句等同于:
String text = Thread.currentThread().getName() + "* run: " + run;
System.out.println(text);
考虑到这种非原子性,事件链(将线程命名为 A 和 B)就像:
/*A*/ String text = Thread.currentThread().getName() + "* run: " + run;
/*B*/ System.out.println(Thread.currentThread().getName() + " setting run to false");
/*B*/ run = false;
/*B*/ System.out.println(Thread.currentThread().getName() + " run:" + run);
/*A*/ System.out.println(text);
/*A*/ if(run == false) {
/*A*/ System.out.println(Thread.currentThread().getName() + "** run: " + run + "\nExiting...");
/*A*/ System.exit(0);
/*A*/ }
由于 lock coarsening 优化,此结果可能也是最常见的结果,其中每个循环都包含所有循环体的全部内容。在这个粗锁之外做的唯一一件事是创建第一个字符串值,它是您观察到的旧值。
有关 JMM 的更多信息,我 once summarized my understanding in a talk. Also, have a look at cache coherence protocols 最终决定可见性。
如果只看volatile变量的读写,那么它们的出现顺序是:
1 - main: read run (run is true)
2 - Thread-0: write run (run is false)
3 - main: read run (run is false)
但控制台输出是单独的操作,不需要在读取后立即发生。 println 的参数评估和调用方法不是原子的。所以我们有更像的东西:
1 - main: read run (run is true)
2 - main: println("Run: true")
3 - Thread-0: write run (run is false)
4 - Thread-0: println("Run: false")
5 - main: read run (run is false)
6 - main: println("Run: false")
这允许在第一次排序之后进行排序,例如:
1 - main: read run (run is true)
3 - Thread-0: write run (run is false)
4 - Thread-0: println("Run: false")
2 - main: println("Run: true")
5 - main: read run (run is false)
6 - main: println("Run: false")
基于 PrintWriter 中的源代码,行:
System.out.println(Thread.currentThread().getName() + " run:" + run);
可以像这样内联:
String x = Thread.currentThread().getName() + " run:" + run;
synchronized(System.out.lock) {
System.out.print(x);
System.out.println();
}
因此 println
中存在同步,但不包括 run
的读取。这意味着 run
的值可以在读取和输出之间发生变化,从而导致输出 run
的旧值。
要获得您期望的输出,同步块需要同时包含 run
和 println
语句的设置。在另一个线程上读取 run
和 println
语句需要在同一个锁上的另一个同步块中。
我有以下代码:
private volatile boolean run = true;
private Object lock =new Object();
.........
Thread newThread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(Thread.currentThread().getName()
+ " run:" + run);
System.out.println(Thread.currentThread().getName()
+ " setting run to false");
run = false;
System.out.println(Thread.currentThread().getName()
+ " run:" + run);
}
}});
newThread.start();
while(true) {//no synchronization, so no coordination guarantee
System.out.println(Thread.currentThread().getName() + "* run: "+run);
if(run == false) {
System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting...");
System.exit(0);
}
}
which generates the following output:
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
Thread-0 setting run to false
Thread-0 run:false
main* run: true <- what causes this???
main** run: false
Exiting...
我试图理解为什么在主线程中发生 main* 运行: true 的异常,考虑到 run
是一个易变字段根据 Java 内存模型规范,Thread-0
中的易失性写入应该立即被 main
线程可见。我知道 Thread-0
中的同步在这里是无关紧要的,但我对 volatile 的这种行为感到困惑。我在这里错过了什么?
另一个甚至更奇怪的 运行 产生了这个:
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main** run: false
Exiting...
Thread-0 run:false
或者这种行为是可以预期的,如果是,为什么?谢谢。
编辑: 正如评论中所问,我正在用我有时但并非总是看到的预期输出更新 post:
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
Thread-0 setting run to false
main* run: true
main* run: true
main* run: true
Thread-0 run:false
main** run: false
Exiting...
也就是说,我不想看到:
main* run: true
出现在
之后Thread-0 run:false
或
main** run: false
Exiting...
出现在
之前Thread-0 run:false
我没有看到问题。这里的锁是没用的。 volatile 也意味着变量在自身内部是同步的。这里发生了什么。每当有多个线程时,每个线程都会自行运行而不关心其他线程。所以在这种情况下,我们有两个线程:main 和 thread-0。 Main 自行运行并到达打印变量 run
的点,因此它打印它。另一个线程休眠了一点(这应该无关紧要,也不应该是让其他线程先工作的方式),然后将变量 run
更改为 false。主线程读取新值存在
跟着时间顺序你就明白了
Thread newThread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(Thread.currentThread().getName()
+ " run:" + run);
System.out.println(Thread.currentThread().getName()
+ " setting run to false");
run = false; //<---- time_4
System.out.println(Thread.currentThread().getName()
+ " run:" + run); //<---- time_5
}
}});
newThread.start();
while(true) { //<---- time_2
System.out.println(Thread.currentThread().getName() + "* run: "+run); //<--- time_3 getting the value of run variable. //<---- time_6 printing
if(run == false) { //<---- time_1 (run == true) // <---- 2nd iteration time_7 (run == false)
System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting..."); //<---- time_8
System.exit(0);
}
}
无论如何,这里是如何修复您的代码以获得预期的输出(注意:volatile 在这里没有做任何事情):
synchronized (lock) {
if(run == false) {
System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting...");
System.exit(0);
}
}
这就是 volatile 对变量的基本作用 run
:
// run = false; //becomes ========
synchronized(someLock) {
run = flase;
}
// =======================
//System.out.println(run); //becomes =========
synchronized(someLock) {
boolean tmpBoolean = run;
}
System.out.println(tmpBoolean);
//=================
至于你程序的正确性,synchronized
块是完全没有必要的。如果您只从单个线程同步锁,那么 JIT compiler eliminates this lock altogether 很有可能。
真正让您感到困惑的是 JMM 保证 volatile
字段保证在写入后可以被其他线程看到其更新值。但是,此保证 并不 暗示写入 volatile
字段的线程会立即传递此值并停止,直到将新值传递给所有其他线程。相反,保证其他线程最终需要看到更新后的值。
这意味着如果线程 A 写入 volatile
字段,线程 B 保证:
- 终于看到这个新值了。
- 不读取之前写入可变字段的任何值及其 "old" 值。
另外,请注意调用 System.out.println
隐式同步 System.out
对象(查看 PrintWriter
代码)。考虑到您在单个监视器上同步两个线程这一事实也解释了观察到的输出。我假设字符串是在 System.out
监视器被您的字段设置线程锁定时创建的。在这种情况下,线程首先创建要写入的字符串,然后等待另一个线程释放此监视器,这就是为什么您通常会观察到带有 "old" 内容的输出。
我的意思是说
System.out.println(Thread.currentThread().getName() + "* run: " + run);
不是原子的。分两步,语句等同于:
String text = Thread.currentThread().getName() + "* run: " + run;
System.out.println(text);
考虑到这种非原子性,事件链(将线程命名为 A 和 B)就像:
/*A*/ String text = Thread.currentThread().getName() + "* run: " + run;
/*B*/ System.out.println(Thread.currentThread().getName() + " setting run to false");
/*B*/ run = false;
/*B*/ System.out.println(Thread.currentThread().getName() + " run:" + run);
/*A*/ System.out.println(text);
/*A*/ if(run == false) {
/*A*/ System.out.println(Thread.currentThread().getName() + "** run: " + run + "\nExiting...");
/*A*/ System.exit(0);
/*A*/ }
由于 lock coarsening 优化,此结果可能也是最常见的结果,其中每个循环都包含所有循环体的全部内容。在这个粗锁之外做的唯一一件事是创建第一个字符串值,它是您观察到的旧值。
有关 JMM 的更多信息,我 once summarized my understanding in a talk. Also, have a look at cache coherence protocols 最终决定可见性。
如果只看volatile变量的读写,那么它们的出现顺序是:
1 - main: read run (run is true)
2 - Thread-0: write run (run is false)
3 - main: read run (run is false)
但控制台输出是单独的操作,不需要在读取后立即发生。 println 的参数评估和调用方法不是原子的。所以我们有更像的东西:
1 - main: read run (run is true)
2 - main: println("Run: true")
3 - Thread-0: write run (run is false)
4 - Thread-0: println("Run: false")
5 - main: read run (run is false)
6 - main: println("Run: false")
这允许在第一次排序之后进行排序,例如:
1 - main: read run (run is true)
3 - Thread-0: write run (run is false)
4 - Thread-0: println("Run: false")
2 - main: println("Run: true")
5 - main: read run (run is false)
6 - main: println("Run: false")
基于 PrintWriter 中的源代码,行:
System.out.println(Thread.currentThread().getName() + " run:" + run);
可以像这样内联:
String x = Thread.currentThread().getName() + " run:" + run;
synchronized(System.out.lock) {
System.out.print(x);
System.out.println();
}
因此 println
中存在同步,但不包括 run
的读取。这意味着 run
的值可以在读取和输出之间发生变化,从而导致输出 run
的旧值。
要获得您期望的输出,同步块需要同时包含 run
和 println
语句的设置。在另一个线程上读取 run
和 println
语句需要在同一个锁上的另一个同步块中。