volatile 的作用和原理是什么?

2025年 阅读约 10 分钟 面试指南 · Java面试 · Java并发

深入解析volatile关键字:保证可见性、禁止指令重排序、内存屏障的四种类型(StoreStore/StoreLoad/LoadLoad/LoadStore)、DCL单例中volatile的必要性、与synchronized的本质区别,附完整原理分析和面试模拟。

一句话总结

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 指令,强制屏障前后的指令不能跨越屏障执行。

屏障类型作用
StoreStorevolatile 写之前:确保前面的普通写已刷新到主内存
StoreLoadvolatile 写之后:确保 volatile 写之后的操作不会被重排到写之前
LoadLoadvolatile 读之后:确保后面的读操作不会被重排到 volatile 读之前
LoadStorevolatile 读之后:确保后面的写操作不会被重排到 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 对比

特性volatilesynchronized
可见性✅ 保证✅ 保证
原子性❌ 不保证✅ 保证
禁止重排✅ 保证✅ 保证(但范围更大)
线程阻塞❌ 不阻塞✅ 可能阻塞
性能高(无锁)较低(有锁开销)
适用场景状态标志、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++ 用 volatilei++ 是读-改-写三步,volatile 不保证原子性用 AtomicInteger
DCL 不加 volatilenew 操作可能重排序,返回半初始化对象必须加 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 的补充而非替代。