volatile 是 Java 提供的一种轻量级的同步机制,它主要用来修饰被多个线程共享的变量。

当一个变量被声明为 volatile 后,它会确保两点:
- 可见性:一个线程对
volatile变量的修改,会立刻对其他所有线程可见。 - 禁止指令重排序:对
volatile变量的读写操作,会插入内存屏障,禁止其前后的指令进行重排序优化。
volatile 的核心作用就是保证变量的可见性,并一定程度上禁止指令重排序以避免一些并发问题。
保证可见性
这是 volatile 最主要、最核心的作用。
问题背景:Java内存模型与工作内存
为了理解可见性,我们需要先了解 Java 内存模型 的一个核心概念:主内存 和 工作内存。

- 主内存:所有线程共享,存储所有实例字段、静态字段和构成数组对象的元素。
- 工作内存:每个线程私有,是线程对主内存中变量操作的一个缓存,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量。
这个机制导致了可见性问题:
- 线程A从主内存读取变量X到自己的工作内存。
- 线程A修改了X的值,但这个修改只存在于线程A的工作内存中,还未写回主内存。
- 线程B也需要读取变量X,它会直接从自己的工作内存(可能为空或旧值)读取,而不会去主内存获取最新的值。
- 这样,线程B就永远看不到线程A对X的修改,造成了数据不一致。
volatile 如何解决可见性问题
当一个变量被 volatile 修饰后,JVM 和编译器会做以下特殊处理:
- 写操作:当线程修改一个
volatile变量时,JVM 会强制将该线程工作内存中的值立刻刷新到主内存中。 - 读操作:当线程读取一个
volatile变量时,JVM 会强制让该线程的工作内存失效,并从主内存中重新读取最新的值。
通过这种“强制刷新”和“强制重读”的机制,volatile 变量的修改对所有线程都是立即可见的。
经典案例:状态标记位
这是 volatile 最常见的应用场景之一。

class Worker implements Runnable {
// 使用 volatile 修饰,确保 stopFlag 的状态对所有线程立即可见
private volatile boolean stopFlag = false;
public void stop() {
this.stopFlag = true;
}
@Override
public void run() {
int i = 0;
// 循环条件依赖于 stopFlag
while (!stopFlag) {
// 模拟工作
i++;
// do something...
}
System.out.println("Worker stopped.");
}
}
public class VolatileExample {
public static void main(String[] args) throws InterruptedException {
Worker worker = new Worker();
new Thread(worker).start();
// 主线程睡眠1秒后,尝试停止工作线程
Thread.sleep(1000);
System.out.println("Main thread is asking to stop the worker...");
worker.stop();
System.out.println("Main thread has asked to stop.");
}
}
如果没有 volatile 会发生什么?
while (!stopFlag) 这行代码可能会被编译器或 JVM 优化,将 stopFlag 的值从主内存读取到工作内存后,循环就不再去主内存检查,而是直接使用工作内存中的值,这样,即使主线程调用了 worker.stop() 将主内存中的 stopFlag 设为 true,工作线程因为看不到这个变化,会陷入死循环。
有了 volatile 之后:
当主线程执行 worker.stop() 将 stopFlag 设为 true 并刷新到主内存后,工作线程在下一次循环读取 stopFlag 时,必须从主内存读取最新的值 true,从而能够正确地退出循环。
禁止指令重排序
volatile 的第二个作用是提供内存屏障,从而禁止指令重排序,这在双重检查锁定实现的单例模式中至关重要。
问题背景:指令重排序
为了提高性能,编译器和处理器可能会对输入的代码进行优化,改变指令的执行顺序,只要最终结果在单线程中是一致的,这在多线程环境下会引发灾难。
volatile 如何禁止重排序
当对一个 volatile 变量进行写操作时,JVM 会在其前后插入一个内存屏障,内存屏障可以禁止其前后的指令进行重排序优化,并保证屏障前的写操作对屏障后的读操作可见。
经典案例:双重检查锁定单例模式
public class Singleton {
// 1. 必须使用 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 = new Singleton(); 并非一个原子操作
在JVM中,这行代码大致可以分解为三个步骤:
- 分配内存:为
Singleton对象分配一块内存空间。 - 初始化对象:调用
Singleton的构造函数,填充对象的各个字段。 - 建立引用:将
instance引用指向分配好的内存地址。
在单线程中,顺序是 1 -> 2 -> 3,但在多线程环境下,由于指令重排序,执行的顺序可能变成 1 -> 3 -> 2。
灾难场景:
- 线程A进入同步代码块,执行
instance = new Singleton();。 - 由于指令重排序,JVM 先执行了步骤1(分配内存)和步骤3(建立引用),但步骤2(初始化对象)还没执行。
instance已经不为null了,但它指向的是一个未初始化完成的对象。- 就在这时,线程B调用
getInstance()方法。 - 线程B执行第一次检查
if (instance == null),发现instance不为null。 - 线程B直接返回
instance。 - 结果:线程B得到了一个尚未初始化完成的
Singleton对象,如果此时去访问其任何字段,都可能导致NullPointerException或其他不可预期的错误。
volatile 如何解决:
通过在 instance 前加上 volatile 关键字,可以确保:
- 分配内存、初始化对象、建立引用这三个步骤的顺序不会被重排序,必须严格按照 1 -> 2 -> 3 的顺序执行。
- 当线程A完成赋值后,线程B能立刻看到这个已经完全初始化好的对象。
volatile 的局限性:不保证原子性
volatile 只能保证可见性和禁止重排序,它不能保证复合操作的原子性。
class Counter {
// volatile 只能保证每次读取到的是最新值,但不能保证 i++ 的原子性
private volatile int i = 0;
public void increase() {
i++; // 这不是一个原子操作
}
}
i++ 实际上包含三个步骤:
- 读取
i的值。 - 将
i的值加1。 - 将新值写回
i。
即使 i 是 volatile 的,也只能保证步骤1读取到的是最新值,步骤3写回后其他线程能立刻看到,但在多线程环境下,线程A可能在执行完步骤1后,被线程B抢占,线程B也执行了完整的三个步骤,然后线程A再继续执行步骤2和3,最终结果会比预期的值小。
解决原子性问题:
对于需要保证原子性的复合操作,应该使用 synchronized 关键字或者 java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)。
volatile vs. synchronized
| 特性 | volatile |
synchronized |
|---|---|---|
| 作用范围 | 只能修饰变量 | 可以修饰方法、代码块 |
| 原子性 | 不保证 | 保证(被同步的代码块/方法是原子的) |
| 可见性 | 保证 | 保证(线程解锁前必须把变量刷新回主内存,加锁时必须清空工作内存) |
| 有序性 | 禁止指令重排序 | 禁止指令重排序(由happens-before原则保证) |
| 性能 | 较高 | 较低(涉及用户态和内核态的切换、锁的获取和释放) |
| 本质 | 轻量级同步机制,基于内存屏障 | 重量级锁机制,基于操作系统层面的互斥锁 |
volatile 是 Java 并发编程中的一个重要工具,它像一把“瑞士军刀”,轻便且有效。
-
使用场景:
- 状态标记位:如上面的
stopFlag,一个线程写,多个线程读。 - 双重检查锁定:确保单例对象的完整初始化过程不会被重排序破坏。
- 一次性安全发布:当需要将一个构造完毕的对象引用发布给其他线程时,可以对该引用使用
volatile。
- 状态标记位:如上面的
-
核心价值:在不需要锁的情况下,解决了可见性和有序性问题,性能开销远小于
synchronized。 -
核心限制:不保证原子性,当需要对一个变量执行“读取-修改-写入”等复合操作时,
volatile无能为力,必须求助于synchronized或java.util.concurrent.atomic包。
