它是什么?
volatile 是 Java 提供的一种轻量级的同步机制,它不像 synchronized 那样可以保证原子性和代码块的互斥执行,但它能保证可见性和禁止指令重排序。

当一个变量被声明为 volatile 后,它就具备了以下两个特性:
- 可见性:当一个线程修改了一个
volatile变量,新值对于其他线程来说会立即变得可见,也就是说,当一个线程读取一个volatile变量时,它总是会看到(读取到)这个变量的最新值。 - 禁止指令重排序:通过插入一个“内存屏障”(Memory Barrier),
volatile关键字可以禁止其前后的指令进行重排序优化,从而保证了代码的执行顺序。
为什么需要 volatile?—— 问题的根源:Java 内存模型
要理解 volatile,必须先了解 Java 内存模型,JVM 为了提高性能,允许线程对共享变量的操作在自己的工作内存(通常是 CPU 高速缓存)中进行,而不是直接在主内存中操作。
这就导致了经典问题:可见性和有序性问题。
可见性问题
假设一个场景:一个主线程启动了一个工作线程,主线程设置一个 flag 为 true 来通知工作线程停止运行。

class Worker implements Runnable {
// 普通变量
private boolean flag = false;
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
while (!flag) {
// do something...
// 如果这里没有适当的同步机制,JVM可能会优化这个循环
// 认为flag在循环中没有被修改,于是直接从自己的工作内存读取flag的初始值false
// 导致while循环永远不会结束,即使主线程已经调用了setFlag(true)
}
System.out.println("Worker stopped.");
}
}
问题分析:
- 主线程将
flag设置为true,这个操作可能只写入了主线程自己的工作内存,还没有立即刷新到主内存。 - 工作线程在自己的工作内存中读取
flag,它看到的仍然是初始值false。 - 即使主线程调用了
setFlag(true),工作线程的while循环也永远不会结束。
有序性问题
指令重排序是 JIT 编译器为了优化性能而做的操作,在某些情况下,重排序可能会导致意想不到的结果。
最经典的例子是双重检查锁定(DCL, Double-Checked Locking)的单例模式实现。
class Singleton {
private static Singleton instance = null; // 没有使用 volatile
private Singleton() {
// 构造函数
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题所在
}
}
}
return instance;
}
}
问题分析:instance = new Singleton(); 这一行代码并非原子操作,大致可以分解为三步:

- 分配对象的内存空间。
- 初始化对象(
Singleton()构造函数)。 - 将
instance引用指向分配好的内存地址。
在指令重排序优化下,执行顺序可能会变成 1 -> 3 -> 2。
灾难性后果:
- 线程 A 进入
synchronized代码块,执行instance = new Singleton();。 - 由于重排序,线程 A 先执行了步骤 1 和 3。
instance已经不为null了,但对象还没有被初始化(步骤 2 未执行)。 - 就在这一刻,线程 B 调用
getInstance()方法,它执行第一次检查if (instance == null),发现instance不为null。 - 线程 B 直接返回
instance,这个instance是一个尚未初始化完成的对象,如果此时线程 B 试图访问instance的任何字段,都可能导致NullPointerException或其他不可预期的错误。
volatile 如何解决这些问题?
volatile 关键字就像一个信号灯,告诉编译器和虚拟机:“对这个变量的操作,请务必按照我写的顺序来,并且修改后要立刻通知其他线程!”
保证可见性
当一个变量被 volatile 修饰后:
- 写操作:当一个线程修改
volatile变量时,JMM 会将该线程工作内存中的值强制刷新到主内存中。 - 读操作:当一个线程读取
volatile变量时,JMM 会使其工作内存中的值失效,并直接从主内存中重新读取最新的值。
这样就解决了上面“Worker线程无法停止”的问题。
class Worker implements Runnable {
// 使用 volatile 修饰
private volatile boolean flag = false;
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
while (!flag) {
// do something...
// 现在每次循环都会从主内存读取flag的最新值
}
System.out.println("Worker stopped.");
}
}
禁止指令重排序
volatile 关键字会插入一个“内存屏障”,内存屏障可以禁止其前后的指令进行重排序,并且可以保证其前面的所有写操作都对其他线程可见。
这解决了双重检查锁定的问题。
class Singleton {
// 使用 volatile 修饰
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁,快速)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(进入同步块)
instance = new Singleton(); // 现在这里是原子的
}
}
}
return instance;
}
}
volatile 会确保 instance = new Singleton(); 的三个步骤(分配内存、初始化、引用赋值)不会被重排序为 1 -> 3 -> 2 的顺序,必须是 1 -> 2 -> 3 完成后,instance 才会不为 null,从而保证了线程安全。
volatile 的局限性:不保证原子性
这是 volatile 最重要也最容易被误解的一点。
场景:一个计数器,多个线程同时对其进行 操作。
class Counter {
// 使用 volatile 修饰
private volatile int count = 0;
public void increment() {
count++; // 这不是原子操作!
}
public int getCount() {
return count;
}
}
count++ 实际上包含三个步骤:
- 读取:读取
count的当前值。 - 修改:将值加 1。
- 写入:将新值写回
count。
即使 count 是 volatile 的,也只能保证步骤 3 的写入对其他线程是可见的。它不能保证这三个步骤作为一个整体(原子性)被执行。
问题分析:
- 线程 A 读取
count(值为 0)。 - 线程 B 读取
count(值也为 0)。 - 线程 A 将 0 加 1,得到 1,并写回
count。 - 线程 B 也将 0 加 1,得到 1,并写回
count。
count 的值是 1,而不是预期的 2,这就是典型的“丢失更新”问题。
如何保证原子性?
- 使用
synchronized关键字。 - 使用
java.util.concurrent.atomic包下的原子类,如AtomicInteger。
volatile vs. synchronized
| 特性 | volatile |
synchronized |
|---|---|---|
| 作用范围 | 只能修饰变量 | 可以修饰方法、代码块 |
| 原子性 | 不保证 | 保证(被同步的代码块/方法) |
| 可见性 | 保证 | 保证(解锁前必须将数据刷新到主内存,加锁时必须清空工作内存) |
| 有序性 | 禁止指令重排序 | 禁止指令重排序(一个线程的解锁 happens-before 另一个线程的加锁) |
| 性能 | 较高,不会引起线程阻塞 | 较低,会引起线程上下文切换和阻塞 |
| 使用场景 | 一个线程写,多个线程读;或状态标志位 | 多个线程读写共享资源,需要保证复杂操作的原子性 |
总结与最佳实践
什么时候使用 volatile?
-
状态标志位:这是
volatile最经典和最合适的用途。private volatile boolean shutdownRequested;。 -
一次性安全发布:当某个引用在构造函数中被正确初始化之后,它被发布给其他线程,可以将该引用声明为
volatile,确保其他线程看到的是完全初始化后的对象,双重检查锁定就是典型例子。 -
“结果缓存”:当某个计算结果需要被多个线程使用,且计算过程不依赖于任何其他状态时,可以使用
volatile缓存结果,避免重复计算。public class CachedCompute { private volatile Result result; public Result compute() { Result res = result; if (res == null) { synchronized (this) { res = result; if (res == null) { res = computeExpensively(); result = res; } } } return res; } }
什么时候不使用 volatile?
- 当你需要保证复合操作的原子性时(如
i++),不要使用volatile,而应使用synchronized或Atomic类。 - 当
volatile变量作为锁,或者一个线程的写操作依赖于另一个线程的读操作时(生产者-消费者模式中的flag),volatile是不够的,必须使用更高级的同步工具,如BlockingQueue或Lock。
volatile 是 Java 并发工具箱中的一把利剑,用对了地方,可以极大地提升程序性能和可读性;用错了地方,则会埋下难以发现的隐患,理解其原理和局限性至关重要。
