一句话总结
volatile 是 Java 中最轻量的同步机制,有两大作用:保证可见性(一个线程修改 volatile 变量后,其他线程立即可见)和禁止指令重排序(通过内存屏障阻止编译器和 CPU 的优化重排)。底层通过 lock 前缀指令实现,将变量修改立即写回主内存,并使其他 CPU 缓存行失效。但 volatile 不保证原子性,i++ 这类复合操作仍需 synchronized 或 AtomicInteger。
初级理解
volatile 的两大作用
| 作用 | 说明 | 示例 |
| 保证可见性 | 一个线程修改后,其他线程立即看到最新值 | 状态标志位、开关变量 |
| 禁止指令重排 | volatile 变量前后的指令不会被重排序 | DCL 单例、懒加载 |
可见性问题演示
public class VisibilityDemo {
private static boolean flag = false; // 不加 volatile
public static void main(String[] args) {
new Thread(() -> {
while (!flag) {} // 线程 B 可能永远看不到 flag 变为 true
System.out.println("线程B退出");
}).start();
Thread.sleep(1000);
flag = true; // 线程 A 修改 flag
// 线程 B 可能永远不会退出!因为 flag 的修改对 B 不可见
}
}
// 解决方案:加 volatile
private static volatile boolean flag = false;
volatile 不保证原子性
public class AtomicityDemo {
private static volatile int count = 0;
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count++; // 不是原子操作!读→改→写 三步
}
}).start();
}
Thread.sleep(2000);
System.out.println(count); // 结果 < 10000,volatile 也救不了
}
}
// 正确做法:用 AtomicInteger
private static AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 保证原子性
一句话总结:volatile = 可见性 + 禁止重排,但不保证原子性。适合一写多读的场景。
中级深入
Java 内存模型(JMM)中的可见性
JMM 规定所有变量存储在主内存中,每个线程有自己的工作内存(CPU 缓存/寄存器)。线程操作变量时先从主内存拷贝到工作内存,修改后再写回主内存。这就导致了可见性问题:线程 A 修改了变量但还没写回主内存,线程 B 读到的是旧值。
volatile 的写操作会立即刷新到主内存,读操作会从主内存重新读取,绕过了工作内存的缓存。
指令重排序 — 为什么需要禁止?
编译器和 CPU 为了性能,会在保证单线程语义正确的前提下重新排列指令的执行顺序。但多线程环境下,重排序可能导致严重问题。
// 经典的重排序问题
int a = 0;
boolean flag = false;
// 线程 A 执行:
a = 1; // ①
flag = true; // ②
// 线程 B 执行:
if (flag) { // ③
int x = a; // ④ 期望 x = 1
}
// 如果 ① 和 ② 被重排序:
// flag = true; // ② 先执行
// a = 1; // ① 后执行
// 线程 B 看到 flag=true 时 a 可能还是 0!
内存屏障(Memory Barrier)— volatile 的底层实现
volatile 通过插入内存屏障来禁止指令重排序。内存屏障是一条 CPU 指令,强制屏障前后的指令不能跨越屏障执行。
| 屏障类型 | 作用 |
| StoreStore | volatile 写之前:确保前面的普通写已刷新到主内存 |
| StoreLoad | volatile 写之后:确保 volatile 写之后的操作不会被重排到写之前 |
| LoadLoad | volatile 读之后:确保后面的读操作不会被重排到 volatile 读之前 |
| LoadStore | volatile 读之后:确保后面的写操作不会被重排到 volatile 读之前 |
// volatile 写操作的内存屏障插入策略
// StoreStore 屏障
volatileVar = newValue; // volatile 写
// StoreLoad 屏障(最重的屏障,几乎刷新所有缓存)
// volatile 读操作的内存屏障插入策略
// LoadLoad 屏障
int v = volatileVar; // volatile 读
// LoadStore 屏障
中级要点:volatile 通过 lock 前缀指令 + 内存屏障实现。StoreLoad 屏障是最重的,会导致 CPU 缓存全部刷新到主内存。
高级拓展
DCL 单例 — 为什么必须用 volatile?
双重检查锁定(DCL)单例中,volatile 的作用是禁止指令重排序,防止返回未初始化完成的对象。
public class Singleton {
private static volatile Singleton instance; // 必须 volatile!
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题在这里!
}
}
}
return instance;
}
}
// new Singleton() 实际分为三步:
// 1. 分配内存空间
// 2. 调用构造方法初始化对象
// 3. 将引用指向内存空间
// 如果没有 volatile,步骤 2 和 3 可能重排序:
// 1. 分配内存空间
// 3. 将引用指向内存空间(此时对象还未初始化!)
// 2. 调用构造方法初始化对象
// 线程 B 在步骤 3 之后、步骤 2 之前调用 getInstance()
// instance != null → 直接返回 → 拿到未初始化的对象!
volatile 的 happens-before 规则
JMM 规定:对一个 volatile 变量的写操作 happens-before 后续对这个变量的读操作。这意味着 volatile 写之前的所有操作的结果,对 volatile 读之后的所有操作都是可见的。
// volatile 的传递性可见
volatile int a = 0;
int b = 0;
// 线程 A:
b = 1; // 普通写
a = 1; // volatile 写
// 线程 B:
if (a == 1) { // volatile 读
// 根据 happens-before 规则
// 这里 b 一定等于 1!
System.out.println(b); // 一定输出 1
}
volatile vs synchronized 对比
| 特性 | volatile | synchronized |
| 可见性 | ✅ 保证 | ✅ 保证 |
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 禁止重排 | ✅ 保证 | ✅ 保证(但范围更大) |
| 线程阻塞 | ❌ 不阻塞 | ✅ 可能阻塞 |
| 性能 | 高(无锁) | 较低(有锁开销) |
| 适用场景 | 状态标志、DCL | 复合操作、临界区 |
高级加分项:能说出 DCL 中 volatile 防止的是 new 操作的重排序(分配内存和初始化),能解释四种内存屏障的作用,能说明 volatile 的 happens-before 传递性。
实战场景
场景一:线程终止标志
public class TaskRunner implements Runnable {
private volatile boolean running = true;
public void run() {
while (running) {
// 执行任务
}
}
public void stop() {
running = false; // 其他线程调用 stop(),running 立即可见
}
}
场景二:懒加载缓存
public class ConfigCache {
private volatile Map<String, String> config = null;
public Map<String, String> getConfig() {
if (config == null) {
synchronized (this) {
if (config == null) {
Map<String, String> temp = new HashMap<>();
temp.put("key", loadFromDB());
config = temp; // volatile 保证完全初始化后才赋值
}
}
}
return config;
}
}
常见坑
| 坑 | 原因 | 解决方案 |
| volatile 修饰数组 | volatile 只保证引用可见,不保证数组元素可见 | 用 AtomicReferenceArray |
| i++ 用 volatile | i++ 是读-改-写三步,volatile 不保证原子性 | 用 AtomicInteger |
| DCL 不加 volatile | new 操作可能重排序,返回半初始化对象 | 必须加 volatile |
| 依赖 volatile 做同步 | volatile 不能替代锁来保护临界区 | 复合操作用 synchronized |
面试模拟
Q:volatile 的作用是什么?
A:两大作用:保证可见性和禁止指令重排序。可见性通过 lock 前缀指令实现,修改后立即写回主内存并使其他 CPU 缓存行失效;禁止重排通过内存屏障实现,volatile 写前后插入 StoreStore 和 StoreLoad 屏障,volatile 读前后插入 LoadLoad 和 LoadStore 屏障。但 volatile 不保证原子性。
Q:DCL 单例为什么用 volatile?
A:防止 new 操作的重排序。new Singleton() 分为分配内存、初始化、赋值引用三步。没有 volatile 时,赋值引用可能先于初始化完成,其他线程看到 instance != null 但拿到的是未初始化的对象。volatile 禁止了这种重排序。
Q:volatile 能保证原子性吗?为什么?
A:不能。volatile 只保证单次读/写的原子性(如 int a = 1),但 i++ 这种复合操作(读→改→写)不是原子的。即使变量是 volatile,多线程同时 i++ 仍会丢失更新。需要用 synchronized 或 AtomicInteger 的 CAS 来保证。
Q:volatile 和 synchronized 的区别?
A:volatile 是轻量级同步,只保证可见性和禁止重排,不阻塞线程,适合状态标志等简单场景。synchronized 保证可见性、原子性和禁止重排,但会阻塞线程,适合复合操作和临界区保护。volatile 是 synchronized 的补充而非替代。