问个简单的 Java 重排序的问题

2020-05-08 22:04:54 +08:00
 guixiexiezou

也学了很久 java 了,但对重排序那块还是不太理解。直接上代码吧

public class App {
    private static boolean flag = false;
    private static int cnt = 0;
    public static void main(String[] argv) throws Exception{
        new Thread(() -> {
            while (!flag) {
                try {
                    // Thread.sleep(2000L);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // System.out.println(cnt);
            }
            System.out.println("----a end");
        }).start();
        Thread.sleep(1000L);
        new Thread(() -> {
            refresh();
        }).start();

    }
    public static void refresh() {
        System.out.println("--stat");
        flag = true;
        cnt = 3;
        System.out.println("---end");
    }
}

执行这个代码,必然是不可以看到----a end这条输出的,但我们把上面的注释代码,随便取消注释 1 个,就可以看到输出----a end,然后程序结束。

有大佬可以简单讲下经验吗

2815 次点击
所在节点    Java
17 条回复
falsemask
2020-05-08 22:40:29 +08:00
https://www.zhihu.com/question/263528143/answer/270308453 和这个类似,这个应该是可见性的问题。Java 多线程可见性问题还挺复杂,我看的时候遇到了好几个问题都没找到答案
zzl22100048
2020-05-08 23:45:40 +08:00
这是可见性问题吧,空循环不让 cpu 没法从主存同步变量
avk458
2020-05-09 00:28:19 +08:00
第一个显视线程注释掉那两行代码后就等于是空 while 了,好像叫做 busy-spin waiting 。这个线程会一直等待下去,当然也就看不到`----a end`吧?
ffkjjj
2020-05-09 09:54:00 +08:00
我觉得, 因为对于 flag 变量, 我们没有显示的对它设置线程的同步, 编译器认为 flag 不会被多个线程共享, 因此代码被 JIT 优化了, 所以出现程序不结束的情况.

```java
if(!flag){
while (true) {
try {
// Thread.sleep(2000L);
} catch (Exception e) {
e.printStackTrace();
}
// System.out.println(cnt);
}
}

```
如果关掉 JIT 编译, 程序就可以正常结束.
ffkjjj
2020-05-09 10:01:44 +08:00
即使是空循环, 系统也不是把所有 cpu 执行时间都分配给此线程.
125113483
2020-05-09 11:10:38 +08:00
变量可见性问题 如果把 flag 加上 volatile 修饰就可以 结束循环 正常输出

把代码 System.out.println 或 Thread.sleep 注释放开也可以结束循环 正常输出 是因为 println 方法被 synchronized 修饰 你可以点到方法里看一下 会刷新 flag 缓存 Thread.sleep 一样也会刷新 都是同步方法 如果你把这两个代码改成其他的 比如 cnt=2 这种非同步代码 一样会出现死循环
yuxing1171
2020-05-09 11:28:34 +08:00
这与重排序有关系? 是可见性的问题吧,第一个线程一直 busy,没时间同步 flag 的值,而如果去过任意注释,CPU 就有时间同步 flag 的值。 用 volatile 修饰 flag,可以起到即时同步的目的。
guixiexiezou
2020-05-09 11:58:38 +08:00
@zzl22100048 原来如此,多谢了
guixiexiezou
2020-05-09 11:58:54 +08:00
@avk458 明白了,多谢
guixiexiezou
2020-05-09 12:02:15 +08:00
@ffkjjj 还真是,测试了下关掉 jit 确实是可以正常输出的,多谢啦
guixiexiezou
2020-05-09 12:07:19 +08:00
@125113483 多谢了,按照你的说法测试了下,确实会如此。但如果强制关闭 JIT,就会发发现可以结束了。所以我还是更倾向于是 JIT 的内部优化结果
guixiexiezou
2020-05-09 12:10:04 +08:00
@yuxing1171 确实是可见性的问题,昨晚说错了。另外你说的`第一个线程一直 busy,没时间同步 flag 的值`这个我是不认可的,安装其他楼层 说法,关掉 JIT 就可以正常结束。我倾向于这种说法,jvm 遇到这种空的死循环,压根就不会去同步数据(开启 JIT 的情况)
ffkjjj
2020-05-09 12:11:12 +08:00
@guixiexiezou #10 但是这种说法, 我不知道怎么解释循环里面去掉注释之后可以同步的问题..
ccpp132
2020-05-09 12:16:54 +08:00
很简单啊,有别的函数调用的话,编译器就不能把 cnt 当作本地变量优化掉,相当于有 memory barrier 的作用。
guixiexiezou
2020-05-09 12:23:57 +08:00
@ccpp132 简单看了下,最主要的原因还是注释的代码都是同步方法,如果我们有别的函数调用的话(非同步方法),会发现结果是一样的,还是会死循环
125113483
2020-05-09 14:50:29 +08:00
@guixiexiezou 不是因为空的死循环不同步数据,你如果 volatile 修饰,空的死循环一样会同步啊 其实 sleep 也不是同步方法,他只是释放了 cpu 资源,cpu 资源一旦空闲 JVM 会完成优化 会同步工作内存和主存 完成内存的可见性
ccpp132
2020-05-09 15:00:17 +08:00
@guixiexiezou 是的,这些函数有 memory barrier 的效果,有些函数没有。和 cpu 资源没有关系的,内存可见性并不要 cpu 空闲。这种明显是没有 memory barrier 的提示被编译器或者 jit 直接优化掉了。如果是可见性的问题稍等就同步了,这个例子里是感觉不出来的。

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

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

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

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

© 2021 V2EX