volatile 与 Java 内存模型
更新时间:2026-09-20。
volatile是最轻量的并发关键字,但也是最容易被误解的——它保证可见性和有序性,却不保证复合操作的原子性。先理解 Java 内存模型(JMM),才能用对它。锁与原子类见 synchronized、JUC 锁与 AQS。
一、JMM:主内存与工作内存
每个线程有自己的工作内存(CPU 缓存/寄存器的抽象),共享变量在主内存里:
线程 A 工作内存:保存共享变量的副本
线程 B 工作内存:保存共享变量的副本
└────── 主内存(共享变量正本)──────┘普通变量一个线程改了,另一个线程何时看到没有保证(可能一直读自己缓存的旧值)。JMM 围绕三个特性定义规则:
| 特性 | 含义 | 普通变量 | volatile | synchronized |
|---|---|---|---|---|
| 原子性 | 操作不可分割 | 单读写有,i++ 没有 | 单读写有,i++ 仍没有 | 有 |
| 可见性 | 写对其他线程立即可见 | 不保证 | 保证 | 保证 |
| 有序性 | 禁止非法重排序 | 编译器/CPU 可重排 | 插入内存屏障禁重排 | 保证 |
二、可见性:一写多读的状态标志
private volatile boolean running = true; // 不加 volatile,子线程可能永远看不到 false
public void run() {
while (running) { /* ... */ } // 主线程置 false 后能立即退出
}
public void stop() { running = false; }对 volatile 变量的写会立即刷回主内存并使其他线程的本地副本失效,读会强制从主内存加载,因此一个线程写、多个线程读的开关/状态标志用它最合适且几乎无锁开销。
三、有序性:禁止重排序与双检锁
编译器和 CPU 为了性能会重排无依赖的指令(单线程语义不变,但多线程会出问题)。volatile 通过在读写前后插入内存屏障,禁止特定方向的重排,并建立 happens-before:对 volatile 变量的写 happens-before 后续所有线程对它的读。
经典用途是双检锁单例:
class Singleton {
private static volatile Singleton instance; // 必须 volatile
public static Singleton get() {
if (instance == null) { // 第一次检查,避免每次加锁
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查,保证只建一个
instance = new Singleton();
// new 分三步:分配内存、初始化、引用赋值;
// 没有 volatile,2/3 可能重排,别的线程拿到未构造完的对象
}
}
}
return instance;
}
}更简单安全的单例可直接用枚举实现。

四、为什么 i++ 仍线程不安全
count++ 是「读 count → 加一 → 写回」三步,volatile 只保证每一步读到的是最新值,但三步之间线程可以交错,两个线程同时读到同样的值、各自写回,结果丢更新。需要原子自增用 AtomicInteger(CAS)或加锁。
坑
- 用 volatile int 做计数器:高并发下丢失更新,换
AtomicInteger/LongAdder。 - 以为加了 volatile 就线程安全:只要临界区里是多个变量或读改写,仍需锁/CAS。
- DCL 单例漏写 volatile:可能发布到「半初始化」对象(指令重排),这是经典隐蔽 bug。
- volatile 引用不等于内部字段也 volatile:数组/集合元素、对象成员的可见性不会被「传染」,需要 Atomic 数组或锁。
- 可见性依赖时序做业务:volatile 不提供等待/通知,线程协作仍要 wait 或 JUC 工具。
总结
JMM 区分主内存与各线程工作内存,普通变量不保证跨线程可见与顺序;volatile 通过把写立即刷回主内存保证可见性、通过内存屏障禁止重排并建立 happens-before 保证有序性,因此非常适合一写多读的状态标志和双检锁单例(防半初始化对象发布),但它不把「读-改-写」变成原子操作,volatile count++ 依然会丢更新,计数与多变量一致性要用 AtomicInteger 或 synchronized。