代码如下
public static void main(String[] args) {
Test a = new Test();
a.start();
for (; ; ) {
if (a.isFlag()) {
System.out.println("1");
}
}
}
static class Test extends Thread {
private boolean flag = false;
public boolean isFlag() {
return flag;
}
@SneakyThrows
@Override
public void run() {
Thread.sleep(1000);
flag = true;
System.out.println(flag);
}
}
野生码仔,对这个问题困惑了一下午
主线程中读到的 flag 值始终为 false
代码改为如下,加上了 else
for (; ; ) {
if (a.isFlag()) {
System.out.println("1");
}else{
System.out.println("2");
}
}
a 线程修改完 flag 值后,主线程是能拿到最新的值的
是否和 cpu 缓存使用的 mesi 协议有关?
1
cmai OP 期待答复
|
2
zhgg0 2020-05-14 20:52:06 +08:00
你点进 println 方法看下。
|
3
yeqizhang 2020-05-14 20:56:02 +08:00 via Android
建议 javap 看下字节码
|
6
cmai OP 2.在没有 else 的情况下,a 线程修改了 flag 的值,main 线程的死循环里为何一直拿不到修改后的值
现在只有这个问题了 |
8
zifangsky 2020-05-14 21:08:12 +08:00
@cmai #6 因为你第一次的代码编译后是这样的:
public static void main(String[] args) { Demo2.Test a = new Demo2.Test(); a.start(); while(true) { while(!a.isFlag()) { ; } System.out.println("1"); } } 然后这个 flag 修改后的值还对主线程是不可见的,所以主线程自然就一直死循环了。 |
9
xzg 2020-05-14 21:09:22 +08:00
你把 flag 定义 volatile 试下,我怀疑是子线程修改后没有及时刷新到主内存。
|
10
zifangsky 2020-05-14 21:14:08 +08:00
@xzg #9 就是你说的这个问题,子线程修改后的 flag 没有机会刷新到主内存,所以最简单的解决办法就是把 flag 变量用 volatile 修饰。
|
11
secondwtq 2020-05-14 21:20:13 +08:00
盲猜编译器是个好人
|
12
cmai OP @zifangsky ok,感谢,这个我了解,但是其实我不是想问这个,因为 volatile 的话,线程对于改变量的操作,会加上内存屏障,从主存中获取, 但是如果我不加 volatile 的话, 我想问线程缓存的副本何时刷新到主存
|
13
cmai OP 并且其他用到该变量的线程何时从主存刷新到自己的线程副本
|
15
cmai OP @cmai fix: ok,感谢,这个我了解,但是其实我不是想问这个,因为 volatile 的话,所有线程对于该变量的操作,会加上内存屏障,从主存中获取, 但是如果我不加 volatile 的话, 我想问线程缓存的副本何时刷新到主存
|
16
cmai OP @xzg 感谢,volatile/sync 是可以达到这样的效果,但是我的问题其实侧重于:主存和线程副本内存是怎么交互的,而不是如何才能达到线程通信的效果
|
18
Lonely 2020-05-14 21:49:44 +08:00 via iPhone 1
第二个问题,应该是即时编译器把 a.isFlag 优化掉了
|
19
momocraft 2020-05-14 22:02:14 +08:00
jmm 不保证的内存同步行为可能被具体 jvm 的具体版本 / 具体硬件 / jit / os 调度 影响
我怀疑研究这个的结论没意义, 就算知道了仍然没法面向这些不可控因素写 jawa 代码 (研究的过程可能有意义) |
20
cmai OP @momocraft 感谢回复,我认为搞懂 main 线程为何在死循环里始终读不到被 a 线程修改后的 flag 的值对我很有帮助,因为和我目前的认知产生了冲突,或者说是我的认知度太浅,所以想究其原因
|
21
cmai OP @Lonely 我会查阅相关资料并且实践,如果确实是这样,并且搞清楚他优化的原因,我回再回来终结此话题的
|
22
secondwtq 2020-05-14 23:09:11 +08:00
实例:bugs.openjdk.java.net/browse/JDK-8003135 [JDK-8003135] HotSpot inlines and hoists the Thread.currentThread().isInterrupted() out of the loop - Java Bug System
|
23
yeqizhang 2020-05-15 00:19:25 +08:00 via Android
用字节码看不出啥问题,
把 if 条件取反,也没啥问题。 可能像楼上说的,这是个 bug…… |
24
1194129822 2020-05-15 00:31:22 +08:00 via iPhone 1
跟 JMM 没什么关系,就是编译器自作聪明的过度优化而已,加了 else 影响了优化。R 大曾经分析过,你去翻翻 R 大的回答就知道了
|
25
cmai OP @yeqizhang 上面说了,其实那个问题 1 和 if 取反没关系,应该是 else 之后的 println 函数里用到了 sync
|
28
suStudent 2020-05-15 11:38:22 +08:00
1:准确来说应该是 synchronized 实现的可见性,所以无所谓锁住是什么对象。
2:感觉可以从线程隔离方面思考。即使子线程已经刷新到主存,但是 main 不会从主存重新获取。 |
29
TuGai 2020-05-15 12:07:01 +08:00 1
去掉 else,加个 -Xint 参数试试
|
30
goldpumpkin 2020-05-15 12:15:06 +08:00
第一个问题,还是没懂。
既然是因为 synchronized 的可见性,就算没有 else,子线程也打印过 flag 啊,主线线程为什么还是获取不到呢? |
32
cmai OP 1.println 为什么可以, 起初我以为是 sync 的原因, 之后发现可能是 jvm 的优化,https://stackoverflow.com/questions/25425130/loop-doesnt-see-value-changed-by-other-thread-without-a-print-statement,这里有一段关键的回答
> it cannot cache the variable during the loop if you call System.out.println |
33
cmai OP 2.-Xint 转成机器码为什么可以,以及 a 线程修改了 flag 的值,main 线程的死循环里为何一直拿不到修改后的值,在上面的链接里可以看到相关答案, 代码可能被优化为了
if (a.isFlag() == false) while (true) {} |
34
TuGai 2020-05-15 13:42:16 +08:00 1
-Xint 不是编译成机器码,而是让 jvm 根据字节码解释执行,不让 JIT 去编译。加了之后可以了说明这是 JIT 编译的问题。https://www.zhihu.com/question/39458585/answer/81521474
|
35
ChanKc 2020-05-15 13:42:37 +08:00
Effective Java 3rd Edition Item 78: Synchronize access to shared mutable data
"This optimization is known as hoisting, and it is precisely what the OpenJDK Server VM does. The result is a liveness failure: the program fails to make progress." |
36
TuGai 2020-05-15 13:43:00 +08:00
R 大牛皮 🐶
|
38
ChanKc 2020-05-15 13:54:58 +08:00
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. 所以我的理解是,JMM 只是规定了程序执行的顺序,即 JLS 里提的 happens-before 顺序。任何不违背这个顺序的重排序的优化都是合法的,因此会出现这种情况 |
39
cmai OP @ChanKc 根据 @TuGai 的回复,RednaxelaFX 的回答和 stackoverflow 的文章, 我认为是 javac 编译出的字节码是正确的执行逻辑, 而 JIT 编译器做了对那段循环代码做了优化处理,flag 变量被当作了循环不变量, 所以当用-Xint 参数,指定 jvm 以字节码执行时,结果是正确的,参考上面的两个链接,https://stackoverflow.com/questions/25425130/loop-doesnt-see-value-changed-by-other-thread-without-a-print-statement;https://www.zhihu.com/question/39458585/answer/81521474
|
40
cmai OP 链接好像混在一起了,不知道 v2 的回复怎么使用 markdown
https://stackoverflow.com/questions/25425130/loop-doesnt-see-value-changed-by-other-thread-without-a-print-statement ------------------------------------------------------------ https://www.zhihu.com/question/39458585/answer/81521474 |
42
cmai OP @ChanKc 明白你的意思,这段代码确实没有命中 happens-before 的其中某项规则,所以编译器可以这样做,但是最终造成了代码出现问题
|
43
Jooooooooo 2020-05-15 16:21:49 +08:00
行为不定义
你主要了解一下 happen before 吧 |
44
cmai OP @Jooooooooo 感谢回复,我认为这段代码和 happens-before 没有直接关系,是 JIT 在不违背 happens-before 原则的情况下优化了此代码,导致程序最终和预期的不一致, 实际用编译出的字节码来执行的话是没有问题的。
|
45
Jooooooooo 2020-05-15 17:50:32 +08:00
@cmai 这段代码就是两个线程没有建立 happen before 原则, 所以一个线程干的事没有道理被另外一个线程看见.
|