关于 jmm 内存模型的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
请不要在回答技术问题时复制粘贴 AI 生成的内容
cmai

关于 jmm 内存模型的问题

  •  1
     
  •   cmai May 14, 2020 3681 views
    This topic created in 2176 days ago, the information mentioned may be changed or developed.

    代码如下

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

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

    你主要了解一下 happen before 吧
    cmai
        44
    cmai  
    OP
       May 15, 2020
    @Jooooooooo 感谢回复,我认为这段代码和 happens-before 没有直接关系,是 JIT 在不违背 happens-before 原则的情况下优化了此代码,导致程序最终和预期的不一致, 实际用编译出的字节码来执行的话是没有问题的。
    Jooooooooo
        45
    Jooooooooo  
       May 15, 2020
    @cmai 这段代码就是两个线程没有建立 happen before 原则, 所以一个线程干的事没有道理被另外一个线程看见.
    About     Help     Advertise     Blog     API     FAQ     Solana     2968 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 69ms UTC 15:04 PVG 23:04 LAX 08:04 JFK 11:04
    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