核心思想一句话总结
volatile 是 Java 提供的一种轻量级的同步机制,它保证了被修饰的变量的可见性和禁止指令重排序,但不保证原子性。

为了彻底理解这句话,我们需要从 Java 内存模型 说起。
背景:Java 内存模型
为了理解 volatile 为什么必要,我们首先要理解一个多线程环境下的核心问题:内存可见性。
在 Java 中,为了提高性能,JMM(Java Memory Model)定义了主内存和工作内存的概念:
- 主内存:所有线程共享,存储了所有实例的字段、静态变量和构成数组对象的元素。
- 工作内存:每个线程私有,是线程的私有数据区,存储了主内存中部分变量的副本。
线程的执行过程如下:

- 线程从主内存中读取变量到自己的工作内存。
- 线程在工作内存中修改变量的值。
- 线程将工作内存中修改后的值写回主内存。
问题就出在这里:由于每个线程都有自己的工作内存,一个线程对变量的修改可能不会立刻被其他线程看到,这就导致了可见性问题。
volatile 的三大作用
volatile 关键字就像一个“旗语”,它向 JVM 和 JIT 编译器发出信号,告诉它们这个变量是“不稳定的”,需要特殊对待。
保证可见性
这是 volatile 最核心、最重要的作用。
当一个变量被 volatile 修饰后:

- 写操作:当线程 A 修改一个
volatile变量时,JMM 会强制将线程 A 工作内存中的值立即刷新到主内存。 - 读操作:当线程 B 读取一个
volatile变量时,JMM 会强制让线程 B 的工作内存失效,并从主内存中重新读取最新的值。
通过这种方式,一个线程对 volatile 变量的修改,对其他线程来说都是立即可见的。
示例:一个经典的可见性问题场景
public class VolatileVisibilityExample {
// private boolean flag = false; // 不使用 volatile
private volatile boolean flag = false; // 使用 volatile
public void writer() {
flag = true; // 1. 修改 flag
System.out.println("Writer thread has set flag to true.");
}
public void reader() {
// while (!flag) { // 如果没有 volatile,这个循环可能永远不会结束
// // 空循环,等待 flag 变为 true
// }
// 使用了 volatile,JVM 会保证 reader 能看到 flag 的最新值
if (flag) {
System.out.println("Reader thread sees flag is true.");
}
}
public static void main(String[] args) throws InterruptedException {
VolatileVisibilityExample example = new VolatileVisibilityExample();
// 启动读线程
new Thread(() -> {
example.reader();
}).start();
// 主线程稍等一下,确保读线程已经进入循环
Thread.sleep(100);
// 启动写线程
new Thread(() -> {
example.writer();
}).start();
}
}
- 不使用
volatile:reader线程可能一直读取到自己工作内存中flag的旧值false,导致while循环无法退出,即使writer线程已经将flag设为true并写回了主内存。 - 使用
volatile:writer线程写回true后,reader线程在下一次读取flag时,会强制从主内存读取,从而得到最新的值true,程序可以正常继续执行。
禁止指令重排序
为了优化性能,编译器和处理器可能会对代码的执行顺序进行调整,只要最终的单线程结果不变,这种调整就被称为指令重排序。
volatile 关键字可以禁止与该变量相关的指令进行重排序,从而保证了程序的有序性。
这在双重检查锁定 实现的单例模式中至关重要。
示例:DCL 中的 volatile
public class Singleton {
// 必须使用 volatile
private static volatile Singleton instance;
private Singleton() {
// 私有构造函数
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
为什么 instance 必须是 volatile?new Singleton() 这一步并非原子操作,大致可以分解为三步:
memory = allocate();// 分配对象的内存空间ctorInstance(memory);// 初始化对象instance = memory;// 将instance引用指向分配的内存地址
如果没有 volatile,由于指令重排序,执行顺序可能变成 1 -> 3 -> 2。
另一个线程在第一次检查 if (instance == null) 时,发现 instance 已经不为 null 了(因为步骤 3 已经完成),于是它返回了一个尚未初始化完成的对象,导致程序出错。
volatile 会确保 instance = new Singleton() 的三个步骤不会被重排序,必须按 1->2->3 的顺序完成,从而避免了这个问题。
不保证原子性
这是 volatile 最容易让人误解的地方。volatile 不能替代 synchronized 来保证复合操作的原子性。
示例:volatile 的非原子性
public class VolatileAtomicityExample {
// volatile 只能保证每次读取和写入的都是主内存的最新值
// 但不能保证 count++ 这个复合操作的原子性
private volatile int count = 0;
public void increment() {
count++; // 这不是原子操作!
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
VolatileAtomicityExample example = new VolatileAtomicityExample();
int threadCount = 1000;
// 创建 1000 个线程,每个线程执行 1000 次 increment
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.increment();
}
}).start();
}
// 等待所有线程执行完毕
Thread.sleep(2000);
System.out.println("Final count: " + example.getCount()); // 结果几乎肯定小于 1,000,000
}
}
为什么 count++ 不是原子操作?
它包含了三个步骤:
- 读取:从主内存读取
count的值到工作内存。 - 修改:在工作内存中将
count的值加 1。 - 写入:将修改后的值写回主内存。
在多线程环境下,可能发生这样的情况:
- 线程 A 读取
count为 100。 - 线程 B 也读取
count为 100。 - 线程 A 将
count加 1,写回 101。 - 线程 B 也将
count加 1,写回 101。 count只增加了 1,而不是 2。
要解决 count++ 的原子性问题,必须使用 synchronized 或者 java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)。
volatile vs. synchronized
| 特性 | volatile |
synchronized |
|---|---|---|
| 作用范围 | 作用于变量 | 作用于代码块或方法 |
| 保证原子性 | 不保证 | 保证 |
| 保证可见性 | 保证 | 保证(释放锁时,会刷回主内存;获取锁时,会从主内存加载) |
| 保证有序性 | 保证(禁止指令重排序) | 保证(一个锁一次只被一个线程持有,保证了临界区内代码的执行顺序) |
| 性能 | 较高,是轻量级的同步机制 | 较低,涉及到用户态和内核态的切换,开销较大 |
| 使用场景 | 适用于一个线程写,多个线程读的场景(如状态标志位) | 适用于多个线程写,或需要保护复杂临界区的场景 |
适用场景
volatile 通常用于以下场景:
- 状态标志位:这是最经典、最安全的用法。
private volatile boolean shutdownRequested;。 - 一次性安全发布:当某个引用的值只在构造函数中初始化,并且没有
this引逸出时,可以将该引用声明为volatile,以确保其他线程能看到已完全初始化的对象,双重检查锁定中的instance。 - 独立观察:某个场景下,一个线程的更新需要被其他线程观察到,但不影响该线程的后续执行逻辑。
volatile变量用于统计信息,其精确性不重要,但必须是最新的。
volatile 的核心三要素:
- 可见性:保证一个线程的修改对其他线程立即可见。
- 有序性:禁止指令重排序,防止因重排序导致的问题。
- 非原子性:不能保证复合操作的原子性,不能替代
synchronized。
volatile 是 Java 并发编程中一个非常强大且高效的工具,但它是一把“手术刀”,精准但有限,在需要保证原子性的复杂场景下,synchronized 和 java.util.concurrent 包下的工具类才是更合适的选择。
