volatile
关键字修饰的变量会通过缓存一致性协议使得多线程访问这个变量的时候,一个线程修改了这个变量,另一个线程就马上能读到。如果没有volatile
,互相不知道对方修改了。
public class Vole {
static Flag flag = new Flag();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag.flag) {
// 什么也不干
// System.out.println("hf");
}
System.out.println("while end");
});
Thread t2 = new Thread(() -> {
flag.flag = false;
System.out.println("flag updated");
});
t1.start();
Thread.sleep(1000);
t2.start();
}
}
class Flag {
/*volatile*/ boolean flag = true;
}
这个代码中,t2
线程修改了flag
为false
,t1
线程却还在一直运行,因为没有volatile
修饰flag
。
如果在while循环里面System.out.println("hf")
注释放开,程序就可以结束。这是为什么呢?是因为这行输出控制台的代码会经过总线所以能察觉到主存中的变量被修改了吗?
先说答案:
这是 JIT干的“好事”,禁用 JIT 就不会有这种问题了(-Xint 禁用,HotSpot下),不信你先试试?
简单的说,JIT 分析代码后发现你这个 while 是个 leaf method(没有调用其他方法),同一线程内不可能有其他代码能观测到 flag 值得变化;所以 JIT 就把 flag 当做了一个“不变量”,将它的读取操作提升(hoist)到循环之外成为局部变量,从而提升执行效率。
提升后的代码会像这样:
提升之后,那怕 flag.flag 已经被修改了,对于你的 while 来说,还是读取的 hoistedFlag 这个提升的局部变量,一直是 true
但增加了 System.out.print 之后,会干扰 JIT,导致 “提升” 不成功,没有把 flag 提到循环之外,所以可以观测到修改
至于为什么会导致提升不成功呢?
你去看 R 大的详细解释,行走的 JVM/Compiler 百科全书 :
https://www.zhihu.com/questio...
不光是 print ,Thread.sleep/wait/yield 也会导致不成功,不过这个和 JIT 就没关系了,猜测可能是线程切换导致缓存失效,重新读取主存(个人看法,没有任何资料支持)
至于有些人看法说是因为 synchronized 导致的缓存失效,这个就是纯扯淡了,synchronized 有锁优化,默认不是重量级锁,不会导致线程切换,更是和你这个提升导致的问题无关。
18.06:再补充下。
JIT 的提升,导致了你的死循环,所以禁用 JIT 可以保证不提升。但 Thread.yield/wait/sleep 可以让线程停止的这个机制,在虚拟机规范和线程规范里,并没有要求。
但根据 R 大的解释,“无法完全内联”会影响 JIT 优化,那么 native 的方法调用都会影响……所以你把 println 换成任意的 native 调用(或者包含)应该都会导致提升失败
JSR-133 里规定了,yeild和sleep(0) 不需要产生任何可观测(observable effects)的效果,yield/sleep 没有任何同步的语义,编译器不用在 yield/sleep 之前将缓存中的值刷新到共享内存中,也不用在 yield/sleep之后 重新读取共享内存的值存到缓存。
综上,虚拟机规范都这么说了,能不能读到最新值你还纠结啥,遵循规范就好,跟着 happens-before 走多香!