volatile 是什么?
volatile 是 Java 语言提供的一种轻量级的同步机制,它是一个修饰符,可以用来修饰成员变量和静态成员变量,当一个变量被声明为 volatile 后,它将具备两种特性:

- 保证可见性
- 禁止指令重排序
核心特性详解
1 保证可见性
这是 volatile 最核心、最重要的特性。
问题背景:JMM (Java Memory Model) 与工作内存
为了理解可见性,我们首先需要了解 Java 内存模型 的一个核心概念:主内存 和 工作内存。
- 主内存:所有线程共享,存储所有实例字段、静态字段和构成数组对象的元素。
- 工作内存:每个线程独有,是线程的私有数据区域,存储了主内存中部分变量的副本。
线程在执行时,不能直接读写主内存中的变量,线程的操作流程如下:

- 从主内存中读取变量到自己的工作内存。
- 对工作内存中的变量副本进行操作。
- 将操作后的结果写回主内存。
这就带来了问题:如果一个线程修改了工作内存中的变量副本,但没有立即写回主内存,那么其他线程就无法感知到这个变化,它们看到的仍然是旧的值,这就导致了可见性问题。
volatile 如何解决可见性问题?
当一个变量被 volatile 修饰后,JMM 会做以下特殊规定:
- 写操作:当线程 A 修改一个
volatile变量时,JMM 会强制要求线程 A 立即将修改后的值刷新到主内存中。 - 读操作:当线程 B 读取一个
volatile变量时,JMM 会强制要求线程 B 从主内存中读取该变量,而不是从自己的工作内存中读取。
通过这种方式,volatile 就像是在线程之间建立了一条直接的、可见的通道,确保一个线程对 volatile 变量的修改,对其他线程来说是立即可见的。

代码示例:经典的可见性问题
下面是一个没有使用 volatile 导致可见性问题的例子。
public class VisibilityProblem {
// private boolean flag = false; // 不使用 volatile
private volatile boolean flag = false; // 使用 volatile 修复
public void writer() {
System.out.println("Writer: Setting flag to true...");
flag = true; // 1. 修改 flag
System.out.println("Writer: Flag has been set to true.");
}
public void reader() {
System.out.println("Reader: Waiting for flag to become true...");
while (!flag) { // 2. 一直读取 flag 的值,如果为 false,则循环
// 空循环,防止 CPU 占用过高
// 如果不加 volatile,这里的 flag 可能永远读取的是工作内存中的旧值 false
}
System.out.println("Reader: Flag is now true. Exiting loop.");
}
public static void main(String[] args) throws InterruptedException {
VisibilityProblem problem = new VisibilityProblem();
Thread readerThread = new Thread(problem::reader);
readerThread.start();
// 让 reader 线程先运行一会儿
Thread.sleep(1000);
Thread writerThread = new Thread(problem::writer);
writerThread.start();
}
}
运行分析(不使用 volatile 时):
readerThread启动,进入while (!flag)循环,它从主内存读取flag的初始值false到自己的工作内存。writerThread启动,执行flag = true,它在自己的工作内存中将flag设为true,但可能没有立即写回主内存。readerThread一直在循环中,因为它每次读取的都是自己工作内存中的false,即使writerThread已经修改了flag,它也感知不到。- 结果:
readerThread的循环永远不会结束,程序会卡住。
运行分析(使用 volatile 时):
writerThread执行flag = true,JMM 强制将true写回主内存。readerThread在下一次循环读取flag时,JMM 强制它从主内存读取,此时它读到了true。- 循环条件
!flag不再满足,readerThread退出循环。 - 结果:程序正常结束。
2 禁止指令重排序
这个特性涉及到有序性。
问题背景:指令重排序
为了提高性能,编译器和处理器可能会对输入的代码进行优化,改变语句的执行顺序,这种优化在单线程中是无害的,但在多线程环境下可能会导致意想不到的错误。
volatile 关键字可以禁止指令重排序,它插入了一个内存屏障。
内存屏障的作用:
- 保证屏障前的所有读写操作,都完成后才能执行屏障后的操作。
- 保证屏障后的所有读写操作,都必须在屏障前操作执行完后才能开始。
这确保了 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;
}
}
为什么必须用 volatile?问题出在 new Singleton() 这句代码上。
它在 JVM 中大致可以分解为三个步骤:
memory = allocate();// 分配对象的内存空间ctorInstance(memory);// 初始化对象instance = memory;// 将instance引用指向分配的内存地址
如果没有 volatile,由于指令重排序,执行顺序可能会变成 1 -> 3 -> 2。
灾难场景:
- 线程 A 进入
synchronized代码块,执行new Singleton()。 - 由于重排序,指令执行了 1 和 3。
instance已经不再为null了,但对象还没有被初始化(ctorInstance未执行)。 - 就在这时,线程 B 调用
getInstance(),它在第一次检查时发现instance != null,于是直接返回instance。 - 线程 B 得到了一个尚未初始化完成的对象,如果此时去访问
instance的任何字段,都可能导致NullPointerException或其他错误。
volatile 如何解决?
volatile 会确保 new Singleton() 的三个步骤不会被重排序,必须按 1->2->3 的顺序执行,这样,只有当对象完全初始化后,instance 才会被赋值,从而避免了上述问题。
volatile 的局限性
volatile 虽然强大,但它不是万能的,它不能替代 synchronized。
volatile 不具备原子性
原子性指的是一个或多个操作,要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。
经典例子:i++ 操作
i++ 这个操作看似简单,实际上在 JVM 中可以分解为三个步骤:
- 读取:从主内存读取
i的值到工作内存。 - 修改:将
i的值加 1。 - 写入:将修改后的
i的值写回主内存。
这三个步骤不是原子操作,如果多个线程同时执行 i++,就会发生问题。
public class VolatileAtomicity {
// volatile 只能保证每次读取都是从主内存,写回也是立即写回主内存
// 但不能保证 i++ 这个复合操作的原子性
private volatile int count = 0;
public void increment() {
count++; // 非原子操作!
}
public static void main(String[] args) throws InterruptedException {
VolatileAtomicity va = new VolatileAtomicity();
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
va.increment();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join(); // 等待所有线程执行完毕
}
// 预期结果是 100 * 1000 = 100000,但实际结果会小于这个值
System.out.println("Final count: " + va.count);
}
}
如何解决原子性问题?
对于需要原子性的复合操作,应该使用 synchronized 关键字或者 java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)。
volatile vs. synchronized
| 特性 | volatile |
synchronized |
|---|---|---|
| 可见性 | 保证 | 保证(释放锁时将变量刷回主内存,获取锁时从主内存读取) |
| 原子性 | 不保证 | 保证(被 synchronized 包裹的代码块是原子执行的) |
| 有序性 | 禁止指令重排序 | 禁止指令重排序(synchronized 同样会禁止临界区内外的重排序) |
| 锁机制 | 不涉及锁,不会引起线程阻塞 | 基于锁,可能会引起线程阻塞和上下文切换 |
| 使用场景 | 适用于一个线程写、多个线程读的场景,如状态标志位 | 适用于多个线程同时读写共享资源的场景,确保线程安全 |
| 性能 | 性能较好,无上下文切换开销 | 性能相对较差,有锁竞争时的开销 |
何时使用 volatile?
当你需要满足以下所有条件时,可以考虑使用 volatile:
- 写操作不依赖于当前值:
flag = true,而不是count++。 - 该变量没有包含在具有其他变量的不变式中:即该变量的状态是独立的。
- 只有一个线程写,多个线程读:这是
volatile最经典的应用场景,如状态标志位。
一句话总结:
volatile保证了变量的可见性和有序性,但不保证原子性,它适用于“一个线程写,多个线程读”的场景,是一种轻量级的同步机制,对于需要原子性的复合操作,请使用synchronized或java.util.concurrent.atomic包。
