Java volatile 关键字深度解析
一、引言
在 Java 并发编程中,volatile 是一个轻量级的同步机制,被称为"轻量级的 synchronized"。它提供了一些有用的保证,但又比 synchronized 的开销小得多。本文将全面解析 volatile 关键字的核心原理、使用场景及注意事项。
二、什么是 volatile
volatile 是 Java 虚拟机提供的一种轻量级同步机制,它修饰变量,具备以下两个核心特性:
- 保证可见性 — 一个线程修改了
volatile变量的值,新值对其他线程立即可见。 - 禁止指令重排序 — 禁止 JVM 和 CPU 对
volatile变量相关的指令进行重排序优化。
注意:
volatile不保证原子性,这是它和锁(synchronized、Lock)最本质的区别之一。
三、内存可见性保证
3.1 为什么会有可见性问题
Java 内存模型(JMM)规定,所有变量存储在主内存中,每个线程拥有自己的工作内存(CPU 缓存)。线程对变量的操作必须在工作内存中进行,不能直接读写主内存。这导致了一个线程修改了变量,其他线程可能看不到。
线程A(工作内存) ←→ 主内存 ←→ 线程B(工作内存)
变量X=0 变量X=0(已过时)
① X = 1
② 刷新到主内存 → X=1
③ 线程B未感知,仍使用 X=0 ❌
3.2 volatile 如何保证可见性
当一个变量被 volatile 修饰时,JVM 会向处理器发送一条 Lock 前缀指令,该指令会:
- 将当前处理器缓存行的数据写回系统内存;
- 这个写回操作会使其他 CPU 缓存了该内存地址的数据无效化(MESI 缓存一致性协议);
- 其他线程读取时发现缓存行无效,必须从主内存重新读取。
用代码直观感受:
public class VisibilityDemo {
// 不加 volatile,程序可能永远不结束
private static boolean flag = true;
// 加 volatile 后,保证可见性
// private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("子线程开始运行...");
while (flag) {
// 循环等待
}
System.out.println("子线程检测到 flag 变化,退出循环");
}).start();
Thread.sleep(1000);
new Thread(() -> {
System.out.println("修改 flag 为 false");
flag = false; // 主线程修改,但子线程可能永远看不到
}).start();
}
}
运行上述代码,如果不加
volatile,子线程可能永远无法退出循环;加上volatile后,子线程能立即感知flag的变化。
四、禁止指令重排序
4.1 什么是指令重排序
为了优化性能,编译器和处理器会对指令进行重排序(Instruction Reordering),前提是重排序不影响单线程的执行结果。但在多线程环境下,重排序可能导致意想不到的问题。
4.2 volatile 如何禁止重排序
JVM 在 volatile 变量的读写操作前后插入内存屏障(Memory Barrier),来禁止特定类型的重排序:
| 内存屏障类型 | 作用 |
|---|---|
| StoreStore 屏障 | 在 volatile 写之前插入,禁止前面的普通写与后面的 volatile 写重排序 |
| StoreLoad 屏障 | 在 volatile 写之后插入,禁止前面的 volatile 写与后面的 volatile 读/写重排序 |
| LoadLoad 屏障 | 在 volatile 读之后插入,禁止后面的普通读与前面的 volatile 读重排序 |
| LoadStore 屏障 | 在 volatile 读之后插入,禁止后面的普通写与前面的 volatile 读重排序 |
简化来说,JMM 对 volatile 的重排序规则是:
- volatile 写:前面的任何操作不能重排序到 volatile 写之后。
- volatile 读:后面的任何操作不能重排序到 volatile 读之前。
举例说明:
class ReorderExample {
int a = 0;
volatile boolean flag = false;
void writer() {
a = 1; // 普通写
flag = true; // volatile 写 —— 禁止前一条指令重排序到这里之后
}
void reader() {
if (flag) { // volatile 读 —— 禁止后一条指令重排序到这里之前
int i = a; // 保证 i 一定为 1(可见性 + 禁止重排序双重保证)
}
}
}
五、happens-before 规则
volatile 关键字在 JMM 中对应一条重要的 happens-before 规则:
对一个 volatile 变量的写操作 happens-before 后续对同一变量的读操作。
这意味着:线程 A 对 volatile 变量 v 的写入,对线程 B 随后读取 v 时可见,并且线程 A 在写入 v 之前的所有操作(对其它变量的修改),对线程 B 也是可见的。
class HappensBeforeExample {
int x = 0;
volatile int v = 0;
void threadA() {
x = 42; // 普通写
v = 1; // volatile 写
}
void threadB() {
if (v == 1) { // volatile 读
// 根据 happens-before 规则,x 一定是 42
System.out.println(x); // 输出 42
}
}
}
JMM 的 happens-before 规则全览(与 volatile 相关的加粗):
| 规则 | 说明 |
|---|---|
| 程序次序规则 | 一个线程内,按照代码顺序,前面的操作 happens-before 后面的操作 |
| 监视器锁规则 | 对一个锁的解锁 happens-before 后续对该锁的加锁 |
| volatile 变量规则 | 对一个 volatile 变量的写 happens-before 后续对该变量的读 |
| 传递性 | 如果 A happens-before B,B happens-before C,则 A happens-before C |
| 线程启动规则 | Thread.start() happens-before 该线程的任何操作 |
| 线程终止规则 | 线程的所有操作 happens-before 对该线程的 join() 返回 |
| 线程中断规则 | 对线程的 interrupt() 调用 happens-before 被中断线程检测到中断事件 |
| 对象终结规则 | 对象的构造函数结束 happens-before finalize() 方法 |
六、典型使用场景
6.1 状态标志(Flag)
最经典的用法 —— 用 volatile 布尔变量控制线程的启停:
class ThreadExample {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// 执行任务...
}
}
}
6.2 一次性安全发布(One-shot Safe Publication)
class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 需要 volatile
}
}
}
return instance;
}
}
6.3 独立观察结果(Independent Observation)
public class SensorReader {
private volatile double temperature;
private volatile double humidity;
public void update(double temp, double hum) {
this.temperature = temp;
this.humidity = hum;
}
public double getTemperature() { return temperature; }
public double getHumidity() { return humidity; }
}
6.4 轻量级读写锁模式
class Counter {
private volatile int value;
public int getValue() { return value; } // 单个读操作,安全
public void setValue(int v) { value = v; } // 单个写操作,安全
}
七、volatile 不保证原子性
这是 volatile 最重要也最容易踩坑的点。volatile 不保证复合操作的原子性,典型的例子就是 i++:
public class NotAtomicDemo {
private static volatile int count = 0;
private static final int THREADS = 20;
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(THREADS);
for (int i = 0; i < THREADS; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
count++; // 本质是三步:读 → 改 → 写,非原子
}
latch.countDown();
}).start();
}
latch.await();
// 期望值:20 * 10000 = 200000
// 实际值:往往小于 200000(如 198763)
System.out.println("最终 count = " + count);
}
}
count++ 的字节码分解:
1. GETSTATIC count // 读取 volatile 变量的值
2. ICONST_1 // 常量 1 入栈
3. IADD // 相加
4. PUTSTATIC count // 将结果写回 volatile 变量
线程 A 在执行完 GETSTATIC 后被挂起,线程 B 修改了 count 并写回,线程 A 恢复后拿着旧值继续操作,导致更新丢失。
解决原子性问题的方式
// 方式一:使用 synchronized
public synchronized void increment() {
count++;
}
// 方式二:使用 AtomicInteger(CAS 实现线程安全)
private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
// 方式三:使用 Lock
private ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
八、volatile vs synchronized 对比
| 对比维度 | volatile | synchronized |
|---|---|---|
| 本质 | 变量修饰符 | 关键字,可修饰方法或代码块 |
| 可见性 | ✅ 保证 | ✅ 保证 |
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 禁止重排序 | ✅ 保证 | ✅ 保证 |
| 阻塞 | 非阻塞,无锁 | 阻塞,使用锁机制 |
| 性能开销 | 较小(仅内存屏障) | 较大(锁的获取/释放、线程上下文切换) |
| 适用场景 | 单一变量的读写、状态标志 | 复合操作、需要原子性的代码块 |
| 能否修饰 | 仅变量 | 方法、代码块、变量 |
| 保证 | 单个 volatile 变量的读写是原子的(64 位类型也保证) | 整个同步块内的所有操作原子 |
何时选择 volatile,何时选择 synchronized
选 volatile 的场景:
- 对变量的写入不依赖其当前值(或只有单一线程写入);
- 变量不与其他状态变量构成不变式约束;
- 访问变量时不需要加锁。
选 synchronized 的场景:
- 需要原子性保护(如
count++、list.add()等复合操作); - 多个变量之间存在约束关系需要同步保护;
- 需要协调多个线程的执行顺序。
九、深入:Double-Checked Locking(DCL)与 volatile
单例模式中的双重检查锁定是 volatile 最著名的应用之一,也是理解其禁止重排序特性的绝佳案例。
public class Singleton {
// volatile 必不可少!
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton();
}
}
}
return instance;
}
}
为什么必须加 volatile?
问题在于 instance = new Singleton() 在 JVM 中分为三步:
memory = allocate(); // 1. 分配对象内存空间
ctorInstance(memory); // 2. 初始化对象(执行构造函数)
instance = memory; // 3. 将 instance 指向分配的内存地址
如果 不禁止重排序,步骤 2 和 3 可能被重排序为:
memory = allocate(); // 1. 分配内存
instance = memory; // 3. 先赋值(对象尚未初始化!)
ctorInstance(memory); // 2. 再初始化
线程 A 执行了 1 → 3(此时对象未初始化),线程 B 进入 getInstance(),发现 instance != null,直接返回一个尚未初始化完成的对象,使用时就可能抛出空指针异常。
volatile 通过禁止指令重排序,保证 1 → 2 → 3 的顺序执行,彻底杜绝此问题。
十、volatile 的实现原理深度解析
10.1 内存屏障(Memory Barrier)
CPU 层面通过内存屏障指令实现:
volatile 写:
StoreStore 屏障
普通写操作
volatile 写操作
StoreLoad 屏障
volatile 读:
volatile 读操作
LoadLoad 屏障
LoadStore 屏障
普通读/写操作
10.2 硬件层面 —— MESI 缓存一致性协议
现代 CPU 通过 MESI 协议(Modified, Exclusive, Shared, Invalid)维护缓存一致性:
- 当 CPU 写入
volatile变量时,发出 Lock# 信号(早期)或使用 缓存锁(现代); - 写入后,使其他 CPU 中该缓存行失效(Invalid);
- 其他 CPU 读取时触发缓存缺失(Cache Miss),从主内存重新加载。
10.3 JVM 层面的实现差异
不同 JVM 实现可能有所差异,但 HotSpot VM 的实现策略是:
- x86 架构:volatile 写比 volatile 读重得多(因为 x86 有强内存模型);
- ARM / 弱内存模型架构:读和写都需要特殊处理。
十一、总结
| 特性 | volatile 保证 |
|---|---|
| 可见性 | ✅ 每次读取都从主内存获取 |
| 原子性 | ❌ 仅保证单次读写操作的原子性(64 位类型也保证) |
| 禁止重排序 | ✅ 通过内存屏障实现 |
| happens-before | ✅ volatile 写 happens-before 后续读 |
最佳实践建议
- 优先用
AtomicXXX或synchronized,只在简单场景使用volatile; volatile+ 不可变对象模式 是非常推荐的并发安全方案;- 不要在
volatile变量上进行依赖当前值的复合操作(如i++); - 理解内存屏障是理解 volatile 的关键,但日常使用只需记住规则;
- DCL 单例中
volatile是必须的,不可省略。
十二、参考资源
- JSR 133 (Java Memory Model) Specification
- 《Java 并发编程的艺术》— 方腾飞
- 《深入理解 Java 虚拟机》— 周志明
- 《Java Concurrency in Practice》— Brian Goetz
- OpenJDK / HotSpot 源码 —
src/hotspot/share/runtime/
写在最后:
volatile是 Java 并发编程中一个看似简单、实则深邃的关键字。理解它需要从 JMM 规范 → 硬件内存模型 → JVM 实现 层层深入。掌握volatile不仅是为了写出正确的并发代码,更是深入理解 Java 内存模型的最佳切入点。