V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
cmai
V2EX  ›  程序员

关于 jmm 内存模型的问题

  •  1
     
  •   cmai · 2020-05-14 20:37:50 +08:00 · 2664 次点击
    这是一个创建于 1435 天前的主题,其中的信息可能已经有所发展或是发生改变。

    代码如下

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

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

    你主要了解一下 happen before 吧
    cmai
        44
    cmai  
    OP
       2020-05-15 16:47:13 +08:00
    @Jooooooooo 感谢回复,我认为这段代码和 happens-before 没有直接关系,是 JIT 在不违背 happens-before 原则的情况下优化了此代码,导致程序最终和预期的不一致, 实际用编译出的字节码来执行的话是没有问题的。
    Jooooooooo
        45
    Jooooooooo  
       2020-05-15 17:50:32 +08:00
    @cmai 这段代码就是两个线程没有建立 happen before 原则, 所以一个线程干的事没有道理被另外一个线程看见.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   4747 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 09:51 · PVG 17:51 · LAX 02:51 · JFK 05:51
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.