关于 jmm 内存模型的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
cmai
V2EX    程序员

关于 jmm 内存模型的问题

  cmai 2020 年 5 月 14 日 3515 次点击
这是一个创建于 2079 天前的主题,其中的信息可能已经有所发展或是发生改变。

代码如下

 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); } } 

背景

野生码仔,对这个问题困惑了一下午

我所了解的知识(不一定正确)

  1. jmm 内存模型中有主存和线程工作内存之分,线程读取一个变量会从主存读到工作内存之中,然后一切操作都是基于工作内存
  2. 工作内存是逻辑概念,实际可能是比如 cpu 缓存
  3. cpu 缓存一致性协议,如果有写操作的话,会通知主存此变量失效,然后其他线程有用这个变量的话会重新读取

代码执行结果

主线程中读到的 flag 值始终为 false

补充

代码改为如下,加上了 else

 for (; ; ) { if (a.isFlag()) { System.out.println("1"); }else{ System.out.println("2"); } } 

a 线程修改完 flag 值后,主线程是能拿到最新的值的

问题

  1. else 到底影响了主存和工作内存之间的哪些交互?
  2. 在没有 else 的情况下,a 线程修改了 flag 的值,main 线程的死循环里为何一直拿不到修改后的值

猜测

是否和 cpu 缓存使用的 mesi 协议有关?

45 条回复    2020-05-15 17:50:32 +08:00
cmai
    1
cmai  
OP
   2020 年 5 月 14 日
期待答复
zhgg0
    2
zhgg0  
   2020 年 5 月 14 日
你点进 println 方法看下。
yeqizhang
    3
yeqizhang  
   2020 年 5 月 14 日 via Android
建议 javap 看下字节码
cmai
    4
cmai  
OP
   2020 年 5 月 14 日
@zhgg0 感谢,看到了 sync,瞬间懂了。。。,是我疏忽了
cmai
    5
cmai  
OP
   2020 年 5 月 14 日
1. else 到底影响了主存和工作内存之间的哪些交互?
本问题已结案,println 中用到了 sync
@zhgg0
cmai
    6
cmai  
OP
   2020 年 5 月 14 日
2.在没有 else 的情况下,a 线程修改了 flag 的值,main 线程的死循环里为何一直拿不到修改后的值
现在只有这个问题了
cmai
    7
cmai  
OP
   2020 年 5 月 14 日
@yeqizhang 我水平可能不太够,暂时还不能从这里下手
zifangsky
    8
zifangsky  
   2020 年 5 月 14 日
@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 修改后的值还对主线程是不可见的,所以主线程自然就一直死循环了。
xzg
    9
xzg  
   2020 年 5 月 14 日
你把 flag 定义 volatile 试下,我怀疑是子线程修改后没有及时刷新到主内存。
zifangsky
    10
zifangsky  
   2020 年 5 月 14 日
@xzg #9 就是你说的这个问题,子线程修改后的 flag 没有机会刷新到主内存,所以最简单的解决办法就是把 flag 变量用 volatile 修饰。
secondwtq
    11
secondwtq  
   2020 年 5 月 14 日
盲猜编译器是个好人
cmai
    12
cmai  
OP
   2020 年 5 月 14 日
@zifangsky ok,感谢,这个我了解,但是其实我不是想问这个,因为 volatile 的话,线程对于改变量的操作,会加上内存屏障,从主存中获取, 但是如果我不加 volatile 的话, 我想问线程缓存的副本何时刷新到主存
cmai
    13
cmai  
OP
   2020 年 5 月 14 日
并且其他用到该变量的线程何时从主存刷新到自己的线程副本
cmai
    14
cmai  
OP
   2020 年 5 月 14 日
@secondwtq 让各位见笑了
cmai
    15
cmai  
OP
   2020 年 5 月 14 日
@cmai fix: ok,感谢,这个我了解,但是其实我不是想问这个,因为 volatile 的话,所有线程对于该变量的操作,会加上内存屏障,从主存中获取, 但是如果我不加 volatile 的话, 我想问线程缓存的副本何时刷新到主存
cmai
    16
cmai  
OP
   2020 年 5 月 14 日
@xzg 感谢,volatile/sync 是可以达到这样的效果,但是我的问题其实侧重于:主存和线程副本内存是怎么交互的,而不是如何才能达到线程通信的效果
xzg
    17
xzg  
   2020 年 5 月 14 日
@cmai 不是很明白,因为 jvm 何时刷新根据 cpu 指令之间的交互、cpu 调度等多种情况,具体你可能要看 jvm 源码了
Lonely
    18
Lonely  
   2020 年 5 月 14 日 via iPhone   1
第二个问题,应该是即时编译器把 a.isFlag 优化掉了
momocraft
    19
momocraft  
   2020 年 5 月 14 日
jmm 不保证的内存同步行为可能被具体 jvm 的具体版本 / 具体硬件 / jit / os 调度 影响

我怀疑研究这个的结论没意义, 就算知道了仍然没法面向这些不可控因素写 jawa 代码 (研究的过程可能有意义)
cmai
    20
cmai  
OP
   2020 年 5 月 14 日 via Android
@momocraft 感谢回复,我认为搞懂 main 线程为何在死循环里始终读不到被 a 线程修改后的 flag 的值对我很有帮助,因为和我目前的认知产生了冲突,或者说是我的认知度太浅,所以想究其原因
cmai
    21
cmai  
OP
   2020 年 5 月 14 日 via Android
@Lonely 我会查阅相关资料并且实践,如果确实是这样,并且搞清楚他优化的原因,我回再回来终结此话题的
secondwtq
    22
secondwtq  
   2020 年 5 月 14 日
实例:bugs.openjdk.java.net/browse/JDK-8003135 [JDK-8003135] HotSpot inlines and hoists the Thread.currentThread().isInterrupted() out of the loop - Java Bug System
yeqizhang
    23
yeqizhang  
   2020 年 5 月 15 日 via Android
用字节码看不出啥问题,
把 if 条件取反,也没啥问题。
可能像楼上说的,这是个 bug……
1194129822
    24
1194129822  
   2020 年 5 月 15 日 via iPhone   1
跟 JMM 没什么关系,就是编译器自作聪明的过度优化而已,加了 else 影响了优化。R 大曾经分析过,你去翻翻 R 大的回答就知道了
cmai
    25
cmai  
OP
   2020 年 5 月 15 日
@yeqizhang 上面说了,其实那个问题 1 和 if 取反没关系,应该是 else 之后的 println 函数里用到了 sync
yeqizhang
    26
yeqizhang  
   2020 年 5 月 15 日
@cmai 嗯,懂了。取反也是因为首先一直 println,把内存同步了。
sonice
    27
sonice  
   2020 年 5 月 15 日
@cmai sync 的是 PrintStream 对象啊,没懂为啥会影响到 flag 的取值。
suStudent
    28
suStudent  
   2020 年 5 月 15 日
1:准确来说应该是 synchronized 实现的可见性,所以无所谓锁住是什么对象。
2:感觉可以从线程隔离方面思考。即使子线程已经刷新到主存,但是 main 不会从主存重新获取。
TuGai
    29
TuGai  
   2020 年 5 月 15 日   1
去掉 else,加个 -Xint 参数试试
goldpumpkin
    30
goldpumpkin  
   2020 年 5 月 15 日
第一个问题,还是没懂。
既然是因为 synchronized 的可见性,就算没有 else,子线程也打印过 flag 啊,主线线程为什么还是获取不到呢?
cmai
    31
cmai  
OP
   2020 年 5 月 15 日
@TuGai 试过了,是可以的,还请老哥指教为什么编译成机器码执行就可以了
cmai
    32
cmai  
OP
   2020 年 5 月 15 日
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
cmai
    33
cmai  
OP
   2020 年 5 月 15 日
2.-Xint 转成机器码为什么可以,以及 a 线程修改了 flag 的值,main 线程的死循环里为何一直拿不到修改后的值,在上面的链接里可以看到相关答案, 代码可能被优化为了
if (a.isFlag() == false) while (true) {}
TuGai
    34
TuGai  
   2020 年 5 月 15 日   1
-Xint 不是编译成机器码,而是让 jvm 根据字节码解释执行,不让 JIT 去编译。加了之后可以了说明这是 JIT 编译的问题。https://www.zhihu.com/question/39458585/answer/81521474
ChanKc
    35
ChanKc  
   2020 年 5 月 15 日
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."
TuGai
    36
TuGai  
   2020 年 5 月 15 日
R 大牛皮
cmai
    37
cmai  
OP
   2020 年 5 月 15 日
@TuGai get 到了
ChanKc
    38
ChanKc  
   2020 年 5 月 15 日
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 顺序。任何不违背这个顺序的重排序的优化都是合法的,因此会出现这种情况
cmai
    39
cmai  
OP
   2020 年 5 月 15 日
@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
cmai
    40
cmai  
OP
   2020 年 5 月 15 日
链接好像混在一起了,不知道 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
ChanKc
    41
ChanKc  
   2020 年 5 月 15 日
@cmai 我只是想从语言规范层面去了解这个问题,而不是依赖于 JVM 的实现
cmai
    42
cmai  
OP
   2020 年 5 月 15 日
@ChanKc 明白你的意思,这段代码确实没有命中 happens-before 的其中某项规则,所以编译器可以这样做,但是最终造成了代码出现问题
Jooooooooo
    43
Jooooooooo  
   2020 年 5 月 15 日
行为不定义

你主要了解一下 happen before 吧
cmai
    44
cmai  
OP
   2020 年 5 月 15 日
@Jooooooooo 感谢回复,我认为这段代码和 happens-before 没有直接关系,是 JIT 在不违背 happens-before 原则的情况下优化了此代码,导致程序最终和预期的不一致, 实际用编译出的字节码来执行的话是没有问题的。
Jooooooooo
    45
Jooooooooo  
   2020 年 5 月 15 日
@cmai 这段代码就是两个线程没有建立 happen before 原则, 所以一个线程干的事没有道理被另外一个线程看见.
关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3902 人在线   最高记录 6679       Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 31ms UTC 05:15 PVG 13:15 LAX 21:15 JFK 00:15
Do have faith in what you're doing.
ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86