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

当一个变量被声明为 volatile 后,它就具备了以下两个特性:
- 可见性:一个线程对
volatile变量的修改,对其他所有线程都是立即可见的。 - 有序性:禁止指令重排序优化,保证了程序的执行顺序符合代码的逻辑顺序。
volatile 解决的核心问题
为了理解 volatile 的作用,我们首先需要了解它在并发环境下要解决的两个核心问题:可见性和指令重排序。
可见性
背景:Java 内存模型
Java 内存模型是定义线程如何与内存进行交互的规范,为了提高性能,JMM 定义了主内存和工作内存的概念:

- 主内存:所有线程共享,存储了所有实例字段、静态字段和构成数组对象的元素。
- 工作内存:每个线程独有,是线程的私有数据区域,存储了主内存中部分变量的副本。
线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存中的变量。
问题所在: 当一个线程修改了一个共享变量的值,它只是在自己的工作内存中修改了这个副本,这个修改并不会立刻同步回主内存,其他线程也无法感知到这个变化,它们可能仍然在使用自己工作内存中的旧值,这就导致了可见性问题。
经典案例:停止线程
public class VolatileDemo implements Runnable {
// 不使用 volatile,线程可能永远不会停止
// private boolean isRunning = true;
// 使用 volatile,可以保证可见性,线程能正常停止
private volatile boolean isRunning = true;
public void setRunning(boolean running) {
isRunning = running;
}
@Override
public void run() {
System.out.println("子线程启动...");
while (isRunning) {
// do something...
// 即使这里没有操作,isRunning 的值也可能不会被刷新
}
System.out.println("子线程结束...");
}
public static void main(String[] args) throws InterruptedException {
VolatileDemo demo = new VolatileDemo();
new Thread(demo).start();
Thread.sleep(1000); // 主线程休眠1秒
System.out.println("主线程即将修改 isRunning 的值...");
demo.setRunning(false); // 主线程修改 isRunning
System.out.println("主线程已修改 isRunning 的值");
Thread.sleep(1000);
System.out.println("主线程结束");
}
}
分析:

- 没有
volatile的情况:主线程将isRunning设为false,这个修改可能只存在于主线程的工作内存中,而子线程一直在循环读取自己的工作内存中的isRunning,它永远是true,导致子线程无法停止。 - 有
volatile的情况:当主线程修改isRunning时,volatile会确保这个修改被立刻刷新到主内存,当子线程需要读取isRunning时,volatile会强制它从主内存中重新读取最新的值,从而看到了false,循环得以终止。
指令重排序
背景:CPU 和编译器的优化
为了提高程序性能,编译器和处理器可能会对输入的代码进行乱序优化,即指令重排序,重排序分为:
- 编译器优化重排序:在不改变单线程程序执行结果的前提下,重新安排语句的执行顺序。
- 处理器运行时重排序:为了充分利用CPU的内部资源(如流水线、乱序执行),处理器可能会对指令进行重排。
问题所在: 在多线程环境下,一些看似没有数据依赖关系的代码,经过重排序后,可能会改变其并发执行的结果,导致程序出错。
经典案例:双重检查锁定实现的单例模式
public class Singleton {
// 没有 volatile,可能会出现问题
// private static Singleton instance = null;
// 使用 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;
}
}
分析:instance = new Singleton(); 这一行代码,大致可以分解为三个步骤:
- 分配对象的内存空间。
- 初始化对象。
- 将
instance引用指向分配的内存地址。
指令重排序的风险:
如果没有 volatile,由于指令重排序,步骤 2 和 步骤 3 的顺序可能会被颠倒,即先执行 3,再执行 2。
发生顺序可能变为:1 -> 3 -> 2
这时,假设线程 A 进入 synchronized 代码块,执行了 instance = new Singleton();,但由于重排序,它先执行了步骤 3(instance 指向了内存地址,但对象还未初始化),就在这一瞬间,线程 B 调用 getInstance() 方法,执行第一次检查 if (instance == null),由于线程 A 已经执行了步骤 3,instance 不再是 null,所以判断为 false,线程 B 直接返回 instance,这个 instance 对象尚未初始化(步骤 2 还没执行),如果线程 B 此时访问 instance 的任何字段,都将会抛出 NullPointerException 或得到错误的数据。
volatile 的作用:
volatile 关键字可以禁止指令重排序,它插入了一个“内存屏障”(Memory Barrier),内存屏障可以确保其前后的指令不会进行重排序优化,加了 volatile 后,instance = new Singleton(); 的三个步骤顺序(1 -> 2 -> 3)将得到保证,从而避免了上述问题。
volatile 的三大特性总结
| 特性 | 作用 | 实现原理 |
|---|---|---|
| 保证可见性 | 当一个线程修改了 volatile 变量,新值对于其他线程来说会立即可见。 |
写操作时,会立刻将值刷新到主内存;读操作时,会直接从主内存读取,并使工作内存中的缓存失效。 |
| 不保证原子性 | volatile 关键字不保证复合操作的原子性。 |
volatile 只是保证了单个读/写操作的原子性,但对于 i++ 这样的“读取-修改-写入”复合操作,无法保证其原子性。 |
| 禁止指令重排序 | 禁止 volatile 变量前后的指令进行重排序优化,保证了代码的执行顺序。 |
通过插入内存屏障,实现内存屏障前后的指令不能重排序。 |
volatile 的局限性:不保证原子性
这是 volatile 一个非常重要的限制。
经典案例:volatile 的 i++ 问题
public class VolatileAtomicityDemo {
// volatile 保证了可见性,但不保证 i++ 的原子性
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
count++; // 非原子操作
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count 的值: " + count); // 结果几乎总是小于 20000
}
}
分析:
count++ 实际上是一个复合操作,包含三个步骤:
- 读取:从内存中读取
count的值。 - 修改:将值加 1。
- 写入:将新值写回内存。
即使 count 是 volatile 的,它也只能保证每次读取都是从主内存读取,每次写入都是立刻刷新到主内存,但它不能保证这三个步骤作为一个整体原子性地执行。
竞态条件:
假设 count 当前值为 10。
- 线程 A 读取
count的值为 10。 - 线程 B 也读取
count的值为 10。 - 线程 A 将值加 1,得到 11,并写回主内存。
count变为 11。 - 线程 B 也将值加 1,得到 11,并写回主内存。
count变为 11。
理想情况下,两次 count++ 应该让 count 变成 12,但实际上只增加了 1,这就是竞态条件。
如何保证原子性?
如果需要保证 count++ 的原子性,应该使用 synchronized 关键字或者 java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)。
volatile vs. synchronized
| 特性 | volatile |
synchronized |
|---|---|---|
| 作用范围 | 作用于变量 | 作用于代码块或方法 |
| 原子性 | 不保证 | 保证(对临界区代码互斥访问) |
| 可见性 | 保证 | 保证(释放锁时刷新内存,获取锁时清空工作内存) |
| 有序性 | 禁止指令重排序 | 禁止指令重排序(happens-before 原则) |
| 性能开销 | 较低 | 较高(涉及用户态到内核态的切换、上下文切换、锁的获取与释放) |
| 使用场景 | 适用于一个线程写,多个线程读的场景。 | 适用于多个线程写,或读写操作复杂的场景。 |
适用场景
volatile 并非万能药,它适用于以下特定场景:
-
状态标记量:如上面的
isRunning例子,一个线程负责修改状态,其他线程根据状态做判断。volatile boolean shutdownRequested; public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do work } } -
双重检查锁定:如单例模式例子,确保
instance对象的创建过程不会被重排序。 -
发布一个事件触发器:当一个状态被更新后,需要触发一系列事件,使用
volatile可以确保所有线程都能看到最新的状态。
volatile是轻量级的同步机制,它解决了多线程环境下的可见性和有序性问题,但不保证原子性。- 它通过内存屏障来实现其语义,确保了变量的修改能被其他线程立即看到,并防止了指令重排序。
- 当你只需要一个线程写、多个线程读,且不涉及复合操作时,
volatile是一个比synchronized更高效的选择。 - 当你需要保证复合操作的原子性时,必须使用
synchronized或java.util.concurrent.atomic包下的原子类。
理解 volatile 是迈向精通 Java 并发编程的重要一步。
