Jakob Jenkov 2023-06-27
Java volatile 关键字用于将一个 Java 变量标记为“存储在主内存中”。更准确地说,这意味着对 volatile 变量的每次读取都会直接从计算机的主内存中读取,而不是从 CPU 寄存器中读取;而对 volatile 变量的每次写入也会直接写入主内存,而不仅仅是写入 CPU 寄存器。
实际上,从 Java 5 开始,volatile 关键字所保证的内容远不止“读写都发生在主内存”这一点。我将在以下章节中详细解释。
变量可见性问题
Java volatile 关键字确保了变量在多线程之间的可见性。这听起来可能有点抽象,让我详细说明一下。
在一个多线程应用程序中,如果线程操作的是非 volatile 变量,出于性能原因,每个线程可能会将变量从主内存复制到 CPU 寄存器中进行处理。如果你的计算机有多个 CPU,那么每个线程可能运行在不同的 CPU 上。这就意味着,每个线程可能会把变量复制到不同 CPU 的寄存器中,如下图所示:

对于非 volatile 变量,JVM 不保证何时将数据从主内存读入 CPU 寄存器,也不保证何时将数据从 CPU 寄存器写回主内存。这会引发一些问题,我们将在接下来的章节中解释。
假设两个或更多线程可以访问一个共享对象,该对象包含如下声明的计数器变量:
public class SharedObject {
public int counter = 0;
}
再假设只有 Thread 1 对 counter 变量进行递增操作,但 Thread 1 和 Thread 2 都会时不时地读取这个变量。
如果 counter 变量没有被声明为 volatile,就无法保证 Thread 1 何时会将 counter 的值从 CPU 寄存器写回到主内存。这意味着 CPU 寄存器中的 counter 值可能与主内存中的值不一致,如下图所示:

这种由于一个线程尚未将变量值写回主内存,导致其他线程看不到最新值的问题,被称为“可见性问题”(visibility problem)——即一个线程的更新对其他线程不可见。
Java volatile 的可见性保证
Java volatile 关键字正是为了解决变量可见性问题而设计的。通过将 counter 变量声明为 volatile,所有对该变量的写入操作都会立即写回主内存,所有读取操作也都会直接从主内存读取。
以下是将 counter 声明为 volatile 的方式:
public class SharedObject {
public volatile int counter = 0;
}
这样声明后,就能保证其他线程能看到对该变量的写入操作。
在上述场景中,如果只有 Thread 1 修改 counter,而 Thread 2 只读取(不修改),那么将 counter 声明为 volatile 就足以确保 Thread 2 总是能看到最新的值。
然而,如果 Thread 1 和 Thread 2 都要对 counter 进行递增操作,仅使用 volatile 就不够了。我们稍后再详细讨论这一点。
完整的 volatile 可见性保证
实际上,Java volatile 的可见性保证不仅限于 volatile 变量本身,还包括与之相关的其他变量。
具体规则如下:
- 如果线程 A 写入了一个
volatile变量,而线程 B 随后读取了同一个volatile变量,那么在线程 A 写入该 volatile 变量之前可见的所有变量,在线程 B 读取该 volatile 变量之后也对线程 B 可见。 - 如果线程 A 读取了一个
volatile变量,那么在线程 A 读取该变量时对其可见的所有变量,也会从主内存中重新读取。
我们用代码示例来说明:
public class MyClass {
private int years;
private int months;
private volatile int days;
public void update(int years, int months, int days) {
this.years = years;
this.months = months;
this.days = days;
}
}
在 update() 方法中,三个变量被赋值,但只有 days 是 volatile 的。
根据完整的 volatile 可见性保证:当 days 被写入时,当前线程可见的所有变量(包括 years 和 months)也会被刷新到主内存。
同样,在读取时:
public class MyClass {
private int years;
private int months;
private volatile int days;
public int totalDays() {
int total = this.days; // 读取 volatile 变量
total += months * 30;
total += years * 365;
return total;
}
public void update(int years, int months, int days) {
this.years = years;
this.months = months;
this.days = days;
}
}
注意:totalDays() 方法首先读取 days。根据 volatile 的语义,此时 months 和 years 也会从主内存中重新加载。因此,你可以确保看到这三个变量的最新值。
指令重排序带来的挑战
出于性能优化,Java 虚拟机(JVM)和 CPU 允许对程序中的指令进行重排序(reordering),只要重排序后的语义与原始顺序一致。
例如,以下代码:
int a = 1;
int b = 2;
a++;
b++;
可以被重排序为:
int a = 1;
a++;
int b = 2;
b++;
语义不变,但执行顺序变了。
然而,当涉及 volatile 变量时,指令重排序就可能带来问题。
回顾之前的 MyClass 示例:
public void update(int years, int months, int days) {
this.years = years;
this.months = months;
this.days = days; // volatile
}
正常情况下,写入 days 会触发 years 和 months 也被刷新到主内存。
但如果 JVM 将指令重排序为:
public void update(int years, int months, int days) {
this.days = days; // volatile
this.months = months;
this.years = years;
}
那么在写入 days 时,months 和 years 还未被赋新值!此时刷新到主内存的仍是旧值,导致其他线程看到不一致的状态。
语义已经发生了改变!
Java volatile 的 happens-before 保证
为了解决指令重排序的问题,Java volatile 关键字除了提供可见性保证外,还提供了 happens-before(先行发生)保证:
写入 volatile 变量前的操作不会被重排到写入之后
如果某些读/写操作在程序顺序上发生在 volatile 写入之前,那么这些操作一定会在 volatile 写入之前完成,并对后续读取该 volatile 变量的线程可见。读取 volatile 变量后的操作不会被重排到读取之前
如果某些读/写操作在程序顺序上发生在 volatile 读取之后,那么这些操作一定会在 volatile 读取之后执行。
注意:允许将“写 volatile 之后”的操作重排到“写 volatile 之前”(只要不影响语义),但不允许将“写 volatile 之前”的操作重排到“写 volatile 之后”。
这一保证确保了 volatile 的可见性语义不会被指令重排序破坏。
volatile 并不总是足够的
即使 volatile 保证了读写都直接访问主内存,但在某些场景下,仅使用 volatile 仍然不足以保证线程安全。
场景一:单写多读 ✅
如果只有一个线程写入 volatile 变量,其他线程只读取,那么 volatile 是足够的。
场景二:多线程写入且新值依赖旧值 ❌
但如果多个线程都要基于当前值计算新值(例如 counter++),那么 volatile 就不够了。
原因:counter++ 实际上包含三步:
- 从内存读取
counter - 加 1
- 写回内存
即使 counter 是 volatile 的,这三个步骤不是原子的。两个线程可能同时读到相同的旧值(比如 0),各自加 1 后都写回 1,导致最终结果应为 2 却变成了 1。
如下图所示:

这就是典型的竞态条件(race condition)。
什么时候 volatile 足够?
总结如下:
- ✅ 仅一个线程写,多个线程读 →
volatile足够。 - ❌ 多个线程写,且写操作依赖变量当前值(如
i++、i = i + 1)→volatile不够,需使用synchronized或java.util.concurrent中的原子类(如AtomicInteger、AtomicLong等)。 - ✅ 多个线程写,但新值不依赖旧值(如直接赋常量
flag = true)→volatile足够。
注意:
volatile保证对 32 位和 64 位变量都有效(包括long和double)。
volatile 的性能考量
- 读写 volatile 变量需要访问主内存(或至少是缓存一致性协议),比访问 CPU 寄存器慢。
- volatile 会禁止某些编译器和 CPU 的优化(如指令重排序),可能影响性能。
因此,只在真正需要保证可见性时才使用 volatile。
实际上,现代 CPU 通常先将寄存器值写入 L1 缓存(速度较快),再由其他硬件同步到主内存。但即便如此,仍应谨慎使用 volatile。