关于 jmm 内存模型的问题

2020-05-14 20:37:50 +08:00
 cmai

代码如下

 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 协议有关?

2869 次点击
所在节点    程序员
45 条回复
cmai
2020-05-14 22:28:14 +08:00
@Lonely 我会查阅相关资料并且实践,如果确实是这样,并且搞清楚他优化的原因,我回再回来终结此话题的
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
2020-05-15 00:19:25 +08:00
用字节码看不出啥问题,
把 if 条件取反,也没啥问题。
可能像楼上说的,这是个 bug……
1194129822
2020-05-15 00:31:22 +08:00
跟 JMM 没什么关系,就是编译器自作聪明的过度优化而已,加了 else 影响了优化。R 大曾经分析过,你去翻翻 R 大的回答就知道了
cmai
2020-05-15 10:01:17 +08:00
@yeqizhang 上面说了,其实那个问题 1 和 if 取反没关系,应该是 else 之后的 println 函数里用到了 sync
yeqizhang
2020-05-15 10:49:52 +08:00
@cmai 嗯,懂了。取反也是因为首先一直 println,把内存同步了。
sonice
2020-05-15 11:17:03 +08:00
@cmai sync 的是 PrintStream 对象啊,没懂为啥会影响到 flag 的取值。
suStudent
2020-05-15 11:38:22 +08:00
1:准确来说应该是 synchronized 实现的可见性,所以无所谓锁住是什么对象。
2:感觉可以从线程隔离方面思考。即使子线程已经刷新到主存,但是 main 不会从主存重新获取。
TuGai
2020-05-15 12:07:01 +08:00
去掉 else,加个 -Xint 参数试试
goldpumpkin
2020-05-15 12:15:06 +08:00
第一个问题,还是没懂。
既然是因为 synchronized 的可见性,就算没有 else,子线程也打印过 flag 啊,主线线程为什么还是获取不到呢?
cmai
2020-05-15 13:02:02 +08:00
@TuGai 试过了,是可以的,还请老哥指教为什么编译成机器码执行就可以了
cmai
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
2020-05-15 13:26:50 +08:00
2.-Xint 转成机器码为什么可以,以及 a 线程修改了 flag 的值,main 线程的死循环里为何一直拿不到修改后的值,在上面的链接里可以看到相关答案, 代码可能被优化为了
if (a.isFlag() == false) while (true) {}
TuGai
2020-05-15 13:42:16 +08:00
-Xint 不是编译成机器码,而是让 jvm 根据字节码解释执行,不让 JIT 去编译。加了之后可以了说明这是 JIT 编译的问题。https://www.zhihu.com/question/39458585/answer/81521474
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
2020-05-15 13:43:00 +08:00
R 大牛皮 🐶
cmai
2020-05-15 13:45:00 +08:00
@TuGai get 到了
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
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
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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/671776

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX