1. 一个并发问题的现场还原:没有 volatile 时线程到底看到了什么
1.1 从一段“看起来没问题”的代码说起
Java 并发编程里,volatile 是一个出现频率极高、但容易让人产生误解的关键字。很多人的第一印象是它保证线程安全,其实它真正的职责是守护跨线程可见性,同时对指令重排做有限度的调节。前阵子我接手一个内部组件,启动后要由一个线程根据状态标志决定是否继续执行,Main 线程准备完成后会把这个标志改掉。代码逻辑非常简单,可压测时就是有极小的概率不停机或者收不到停止信号。一开始怀疑线程调度问题,查了半天,最后把目光停在了一个没有修饰的 boolean 变量上。
当时这段代码从概念上可以简化成这样:
java复制public class FlagDemo {
private static boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
int count = 0;
while (running) {
count++;
}
System.out.println("worker 退出,处理次数:" + count);
});
worker.start();
Thread.sleep(200);
running = false;
worker.join();
}
}
这段代码的问题在于:running 不是一个 volatile 字段,也没有被 synchronized 保护。按照 Java 内存模型的规则,它不保证一个线程对 running 的修改一定能“及时”被另一个线程看到。我实际运行时,不同 JDK 版本、不同机器上面的表现都不一样:有些环境能正常退出,有些环境会长时间卡在 busy loop 里。真正理解这个问题,得回到 Java 内存模型(JMM)的基本概念上。
1.2 问题真正的来源:主内存和线程私有工作内存
JMM 在设计时引入了一个抽象模型:每个线程有自己的“工作内存”,里面保存着它使用的共享变量的副本;线程对变量的读写,首先发生在自己的工作内存里,然后再同步回主内存。这个抽象对应的是 CPU 寄存器、L1/L2 缓存和多核共享内存的真实机制。注意它不会保证普通变量的写入在哪个时点被刷回主内存,也不保证一个线程何时从主内存读取最新值。
想象一个办公室里有一个共享的黑色记事本,这是“主内存”。张三、李四手里各有一个记事本的复印件,这是“工作内存”。张x在自己复印件上写了一行新记录,只要他不把复印件翻回黑本子,李四看自己手里的复印件永远看不到这条新记录。volatile 干的事情,就相当于强制约定:写到 volatile 变量时,必须立刻把整个“当前状态”同步到共享本子上;读 volatile 变量时,必须放弃手中复印件,重新从共享本子上抄一遍。
如果没有 volatile,编译器、JIT 以及 CPU 都有权做各种优化——它们可以把 while (running) { count++; } 变成 while (true) 的死循环,因为当前线程没有修改 running,为什么每轮循环都要去主内存读取一遍?这个优化在单线程中是完全正确的,但放到多线程里,就破坏了可见性。所以一句话总结:普通变量在跨线程场景下是不保证可见性的,volatile 才是专门用来解决这个问题的关键字。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. volatile 的承诺和边界:可见性包含什么、不包含什么
2.1 volatile 写读建立的“先发生”规则
volatile 的核心语义在 JLS(Java 语言规范)里给得很明确:对一个 volatile 变量的写
