未用volatile线程仍感知变量变更的原因及JVM缓存刷新场景咨询
代码示例
public class VisibleDemo { private boolean flag; public VisibleDemo setFlag(boolean flag) { this.flag = flag; return this; } public static void main(String[] args) throws InterruptedException { VisibleDemo t = new VisibleDemo(); new Thread(() -> { long l = System.currentTimeMillis(); while (true) { if (System.currentTimeMillis() - l > 600) { break; } } t.setFlag(true); }).start(); new Thread(() -> { long l = System.currentTimeMillis(); while (true) { if (System.currentTimeMillis() - l > 500) { break; } } while (!t.flag) { // if (System.currentTimeMillis() - l > 598) { // // } } System.out.println("end"); }).start(); } }
现象说明
如果注释掉以下代码块,程序永远不会输出"end":
if (System.currentTimeMillis() - l > 598) { }
添加该代码块后,程序的表现分为三种情况:
- 判断值小于598(比如550)或者无此代码块时,不会输出
"end"; - 判断值等于598时,可能输出
"end"; - 判断值大于598时,每次都会输出
"end"。
注:
- 598是本地测试得出的数值,不同设备可能有差异;
flag未加volatile修饰,却能在某些情况下感知到最新值。
问题解答
1. 出现上述现象的原因是什么?
这事儿的核心原因是JIT编译的优化加上线程工作内存与主内存的同步时机。
第二个线程进入while (!t.flag)循环后,因为flag没加volatile修饰,JIT编译器会把这个循环优化成死循环——它默认其他线程不会修改flag的值,直接把flag的缓存值加载到寄存器,之后再也不从主内存读取新值,自然退不出循环,也就打不出"end"。
加了那个if判断后,情况就变了:System.currentTimeMillis()是个本地方法,JVM执行本地方法时,会禁用部分JIT优化,还会触发工作内存和主内存的同步。当判断值大于598时,第二个线程执行这个if的时间,已经晚于第一个线程设置flag=true的时间(第一个线程要等600毫秒才修改),这时候同步主内存就能读到最新的flag值,自然退出循环输出"end"。
要是判断值等于598,两个线程的操作时间点刚好凑到一块,存在竞态:如果第二个线程执行if时刚好触发同步,且第一个线程已经修改了flag,就能读到新值;不然还是读缓存,继续死循环。而判断值小于598的话,第二个线程执行if的时候,第一个线程还没修改flag,同步后主内存里的flag还是false,之后JIT又把循环优化成死循环,自然还是打不出"end"。
2. JVM线程的工作缓存何时会与主内存进行刷新同步?
JVM没有硬性规定固定的同步时间点,主要看内存屏障、JVM实现和硬件架构,常见触发场景有这些:
- 读写
volatile变量:写volatile变量时,会强制把工作内存的修改刷到主内存;读的时候,会强制从主内存读取最新值。 - 进出
synchronized代码块/方法:解锁操作会把工作内存的修改刷到主内存,加锁时会从主内存读取最新数据。 - 执行本地方法:像
System.currentTimeMillis()、Thread.sleep()这类native方法,JVM在调用前后可能会插入内存屏障,触发同步。 - 线程上下文切换:线程被挂起、调度切换的时候,可能会把工作内存的修改刷到主内存,下次恢复时再从主内存读取新数据。
- JIT编译器的优化:某些情况下,JIT会根据代码逻辑插入内存屏障,避免优化导致的内存可见性问题(就像刚才例子里本地方法触发的同步)。
内容的提问来源于stack exchange,提问作者qinghua
相关产品推荐
相关产品推荐

