也学了很久 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
,然后程序结束。
有大佬可以简单讲下经验吗
1
falsemask 2020-05-08 22:40:29 +08:00 1
https://www.zhihu.com/question/263528143/answer/270308453 和这个类似,这个应该是可见性的问题。Java 多线程可见性问题还挺复杂,我看的时候遇到了好几个问题都没找到答案
|
2
zzl22100048 2020-05-08 23:45:40 +08:00 via iPhone
这是可见性问题吧,空循环不让 cpu 没法从主存同步变量
|
3
avk458 2020-05-09 00:28:19 +08:00
第一个显视线程注释掉那两行代码后就等于是空 while 了,好像叫做 busy-spin waiting 。这个线程会一直等待下去,当然也就看不到`----a end`吧?
|
4
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 编译, 程序就可以正常结束. |
5
ffkjjj 2020-05-09 10:01:44 +08:00
即使是空循环, 系统也不是把所有 cpu 执行时间都分配给此线程.
|
6
125113483 2020-05-09 11:10:38 +08:00
变量可见性问题 如果把 flag 加上 volatile 修饰就可以 结束循环 正常输出
把代码 System.out.println 或 Thread.sleep 注释放开也可以结束循环 正常输出 是因为 println 方法被 synchronized 修饰 你可以点到方法里看一下 会刷新 flag 缓存 Thread.sleep 一样也会刷新 都是同步方法 如果你把这两个代码改成其他的 比如 cnt=2 这种非同步代码 一样会出现死循环 |
7
yuxing1171 2020-05-09 11:28:34 +08:00
这与重排序有关系? 是可见性的问题吧,第一个线程一直 busy,没时间同步 flag 的值,而如果去过任意注释,CPU 就有时间同步 flag 的值。 用 volatile 修饰 flag,可以起到即时同步的目的。
|
8
guixiexiezou OP @zzl22100048 原来如此,多谢了
|
9
guixiexiezou OP @avk458 明白了,多谢
|
10
guixiexiezou OP @ffkjjj 还真是,测试了下关掉 jit 确实是可以正常输出的,多谢啦
|
11
guixiexiezou OP @125113483 多谢了,按照你的说法测试了下,确实会如此。但如果强制关闭 JIT,就会发发现可以结束了。所以我还是更倾向于是 JIT 的内部优化结果
|
12
guixiexiezou OP @yuxing1171 确实是可见性的问题,昨晚说错了。另外你说的`第一个线程一直 busy,没时间同步 flag 的值`这个我是不认可的,安装其他楼层 说法,关掉 JIT 就可以正常结束。我倾向于这种说法,jvm 遇到这种空的死循环,压根就不会去同步数据(开启 JIT 的情况)
|
13
ffkjjj 2020-05-09 12:11:12 +08:00
@guixiexiezou #10 但是这种说法, 我不知道怎么解释循环里面去掉注释之后可以同步的问题..
|
14
ccpp132 2020-05-09 12:16:54 +08:00
很简单啊,有别的函数调用的话,编译器就不能把 cnt 当作本地变量优化掉,相当于有 memory barrier 的作用。
|
15
guixiexiezou OP @ccpp132 简单看了下,最主要的原因还是注释的代码都是同步方法,如果我们有别的函数调用的话(非同步方法),会发现结果是一样的,还是会死循环
|
16
125113483 2020-05-09 14:50:29 +08:00
@guixiexiezou 不是因为空的死循环不同步数据,你如果 volatile 修饰,空的死循环一样会同步啊 其实 sleep 也不是同步方法,他只是释放了 cpu 资源,cpu 资源一旦空闲 JVM 会完成优化 会同步工作内存和主存 完成内存的可见性
|
17
ccpp132 2020-05-09 15:00:17 +08:00
@guixiexiezou 是的,这些函数有 memory barrier 的效果,有些函数没有。和 cpu 资源没有关系的,内存可见性并不要 cpu 空闲。这种明显是没有 memory barrier 的提示被编译器或者 jit 直接优化掉了。如果是可见性的问题稍等就同步了,这个例子里是感觉不出来的。
|