You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

未用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 05:45:41