1. volatile解决的到底是什么问题
1.1 一次线上事故:线程停止标志没生效
先讲个我实际遇到过的事儿。前几年维护一个老项目,里面有个后台任务线程,代码逻辑大概是这样的:
java复制public class DataPoller {
private boolean stop = false;
public void start() {
new Thread(() -> {
while (!stop) {
// 拉取数据并处理
}
}).start();
}
public void shutdown() {
stop = true;
}
}
一开始运行得好好的,后来随着请求量上来,运维通知说某个节点的任务进程一直在空转,CPU飙到80%多。我们当时排查了半天,代码逻辑看起来没问题:明明调了shutdown(),stop也置为true了,可循环就是跳不出去。最后实在没办法,重启服务才恢复正常。
等事后复盘才发现,问题就出在这个stop变量上。它没有用volatile修饰。在主线程里改了stop,工作线程在另一个CPU核心上却一直读不到这个变更,所以循环永远走不出来。
这个案例基本就是Java并发里"可见性"问题最典型的场景。你如果没研究过Java并发,第一反应肯定是"这怎么可能",但事实就是,在多线程环境下,一个线程对共享变量的修改,其他线程未必能立刻看到。而volatile关键字,就是用来解决这类问题的核心工具之一。
1.2 先搞懂JMM,才能看懂volatile
想彻底弄明白volatile,绕不开Java内存模型(Java Memory Model,简称JMM)。说句实话,很多人学并发上来就背synchronized、volatile的区别,但不理解JMM的话,背完也是一头雾水,换个场景照样出错。
JMM是Java虚拟机规范中定义的一套抽象内存模型,它规定了两件事:一是线程之间的共享变量存在哪儿,二是线程怎么和这些变量交互。JMM把内存分成了"主内存"和"工作内存"。主内存是所有线程共享的,存放所有共享变量;而每个线程有自己的工作内存(可以类比成CPU缓存),线程操作变量的流程是:先从主内存把变量拷贝到自己的工作内存,然后在工作内存里读写,最后再刷回主内存。
这里就出现了问题:线程A改了工作内存里的变量副本,但还没刷回主内存;或者刷回去了,线程B却还没从主内存重新拉取最新值。结果就是,A的修改对B来说"不可见"。这就是上面那个线上事故发生的根本原因。
所以volatile干的事情,就是为这个"拷贝-回写"过程加了一层强约束。我们在下一节详细看它到底约束了什么。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. volatile的两大核心能力:可见性与有序性
2.1 可见性:让所有线程都能拿到"最新值"
volatile最直接的作用就是保证可见性。用volatile修饰的变量,在写操作发生后,会立即对所有线程可见。具体机制是:一个线程写volatile变量时,JMM会把这个变量在工作内存中的新值强制刷新到主内存;而其他线程在读这个volatile变量时,会强制从主内存中重新加载,而不是直接用工作内存里的旧副本。
你可以理解成,volatile变量就像贴了公告栏的公告,每次有人改了内容,系统就会全公司广播一遍,所有人下次看公告时都是最新版,绝不允许有人在自己的文件夹里保留一份旧版。
回到前面那个事故案例,修复方式就是给stop加上volatile修饰:
java复制public class DataPoller {
private volatile boolean stop = false;
// 其余代码不变
}
加上这一处修改,工作线程在循环里读stop时,每次都会去主内存拿最新值,只要shutdown()方法一改动,循环马上就能感知到。
这里要强调一点:volatile保证的是"一个变量在多个线程间的可见性",它管不了复合操作的原子性。这两者的区别非常关键,我在2.3节会专门讲。
2.2 有序性:禁止指令重排序,防止"看起来乱来"的代码
除了可见性,volatile还有第二项能力:禁止指令重排序。这块在面试里也是常客,但我发现很多人理解得很浅,只知道"禁止重排序"这几个字,说不清到底防的是什么。
先解释一下指令重排序。CPU和编译器为了提升执行效率,会在不影响单线程执行结果的前提下,对指令的执行顺序进行调整。比如你写的是:
java复制int
