Java 并发编程:volatile (可见性 / 指令重排序 / 与 synchronized 对比)
一、前言
volatile 关键字是 Java 提供的“轻量级同步机制”,它的核心作用有两个:一是保证变量的可见性,二是禁止指令重排序。但它不能保证原子性,这是它和 synchronized 最大的区别。**
二、核心概念通俗+专业解析
1. 保证可见性(线程间通信)
- 通俗解释:想象 CPU 有很多个“小账本”(工作内存),每个线程拿一个账本记账。
volatile就像是一个“实时同步器”,当一个线程修改了变量,它会强制把修改的结果立刻写回“总账”(主内存),并让其他线程的“小账本”失效,强制它们去总账里重新读最新的数据。 - 专业术语:解决了 JMM(Java内存模型) 中的可见性问题,强制线程读写主内存,绕过工作内存缓存。
- 代码示例(解决可见性问题):
public class VolatileVisible {
// 1. 加上 volatile 保证可见性
private volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
// 线程A:不断检查 flag
new Thread(() -> {
while (flag) {
// 如果不加 volatile,这里可能一直卡在循环,无法感知主线程修改
}
System.out.println("线程A结束:flag变为false");
}, "线程A").start();
Thread.sleep(1000); // 主线程休眠1秒
// 主线程:修改 flag
flag = false;
System.out.println("主线程:flag已修改为false");
}
}
*解释:如果不加 volatile,线程A可能一直缓存着 flag = true 的副本,主线程改成 false 后,线程A感知不到,导致程序死循环。
2. 禁止指令重排序
- 通俗解释:为了优化性能,CPU 和 JVM 可能会打乱代码的执行顺序(只要最终结果不变)。
比如单例模式里的instance = new Singleton(),原本应该是 1.分配内存 → 2.初始化对象 → 3.赋值引用。
如果重排序变成 1 → 3 → 2,线程A刚赋值完引用(还没初始化对象),线程B进来判断instance != null,就会拿到一个**“残缺(半初始化)”**的对象。volatile就是通过 内存屏障(Memory Barrier) 来禁止这种危险的重排序。 - 专业术语:通过插入 内存屏障(Memory Barrier),锁定指令执行顺序,保证 Happens-Before 规则。
- 核心应用:DCL 单例模式(这是 volatile 最经典的面试考点)。
这里看博客:
三、避坑核心:为什么 volatile 不能保证线程安全?(原子性)
这是最大的陷阱,一定要重点讲。
- 通俗解释:
volatile能保证“看到最新的数据”,但保证不了“操作数据的原子性”。
想象count++这个动作,它是由“读数据、加一、写回数据”三步组成的。
即使count是volatile的,两个线程同时读到 100,各自加一变成 101 再写回去,最后结果可能是 101 而不是 102。因为中间步骤可能被打断!
什么是复合操作?
通俗解释:count++ 看似是一步操作,其实拆成了三步:
- 读:从主内存读取
count的值(比如 100)。 - 改:在工作内存中加 1(变成 101)。
- 写:把 101 写回主内存。
这三步不是原子的!多线程同时执行时,可能读到同一个值,导致最终结果小于预期。
- 专业结论:
count++是 读 - 改 - 写 的 复合操作,不具备原子性。volatile只能保证可见性和有序性,保证不了原子性。 - 代码对比(避坑演示):
/**
* 演示 volatile 不保证原子性(count++ 问题)
*/
public class VolatileAtomic {
// 私有静态 volatile 变量
private static volatile int count = 0;
public static void increment() {
// 这行代码不是原子性的!
// 复合操作:count++ 非原子性
// 分为:读count -> 加1 -> 写count
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increment();
}
}).start();
}
// 等待所有线程结束
while (Thread.activeCount() > 1) {}
// 预期 10000,实际可能小于 10000
System.out.println("最终结果: " + count);
}
}
解释:这段代码打印出来的结果通常不是 10000。因为 count++ 在多线程下是非原子操作,volatile 救不了。必须用 synchronized 或者 AtomicInteger 才能解决。
四、复杂概念:指令重排序与内存屏障(深入理解)
- 为了提高执行效率,CPU 或 JVM 可能会打乱代码的执行顺序(只要保证最终结果不变)。
- 复杂概念:在 JMM(Java 内存模型) 中,为了优化性能,编译器会进行指令重排序。但
volatile会通过 内存屏障(Memory Barrier) 锁住指令顺序,保证 Happens-Before 规则。
如果问深一点,你可以这样补充(用比喻):
- 什么是指令重排序?
就像做饭,顺序本来是:洗米 → 煮饭 → 洗菜。
重排序后可能变成:洗米 → 洗菜 → 煮饭。
只要最后饭是熟的,菜是洗好的,结果没问题,JVM 就可以换顺序。
但单例模式下,必须按“分配内存→初始化→赋值”的顺序,否则就出 bug。 - volatile 怎么禁止重排序?(内存屏障)
JVM 会在指令序列中插入“内存屏障”。
-
- 写操作前:插入屏障,确保写操作之前的所有操作都执行完并刷新到主内存。
- 读操作后:插入屏障,确保读操作后都去主内存拿最新数据。
这就相当于在厨房贴了“禁止调整烹饪顺序”的告示。
代码演示:
/**
* 演示指令重排序与 volatile 的防护
*/
public class ReorderExample {
int a = 0;
// boolean flag = false; // 未使用volatile,可能重排序
volatile boolean flag = false; // 使用volatile,禁止重排序
public void writer() {
a = 1; // 步骤1
flag = true; // 步骤2
}
public void reader() {
if (flag) { // 步骤3
int i = a * a; // 步骤4
System.out.println(i);
}
}
// 测试主类
public static void main(String[] args) {
ReorderExample example = new ReorderExample();
// 模拟多线程环境
new Thread(() -> example.writer(), "写线程").start();
new Thread(() -> example.reader(), "读线程").start();
// 这里只是演示,实际输出可能因执行时机而异
// 但加了volatile后,能保证看到的a一定是1
}
}
未使用 volatile(可能重排序):线程 A 执行 writer() 时,JVM 可能重排序为:先写 flag = true,后写 a = 1。线程 B 读取时,可能看到 flag=true 但 a=0,输出 0。
使用 volatile(禁止重排序):volatile flag 会在 a=1 前后插入内存屏障,强制:
- 写屏障:
a=1必须在flag=true之前执行。 - 读屏障:读取
flag时,必须去主内存读取最新值,且a的赋值必须在flag判定之后。
五、volatile vs synchronized(面试必问对比)
我帮你整理成了一张表,面试时娓娓道来即可。
| 维度 | volatile | synchronized |
|---|---|---|
| 核心属性 | 轻量级,关键字 | 重量级,同步机制/锁 |
| 保证原子性 | ❌ 不能(最核心的坑点) | ✅ 能(锁机制保证) |
| 保证可见性 | ✅ 能(强制刷新主内存) | ✅ 能(释放锁前必刷主内存) |
| 禁止重排序 | ✅ 能(内存屏障) | ✅ 能(锁内代码整体有序) |
| 性能 | ⭐⭐⭐⭐⭐ 极高(无锁切换) | ⭐⭐⭐ 较高(涉及线程阻塞/切换) |
| 使用场景 | 单一变量的读写(如标志位、双重检查锁 DCL) | 复合操作(count++)、多线程复杂协作 |
volatile 和 synchronized 是 Java 并发中两个完全不同维度的工具。
首先,核心区别在于 “原子性”。volatile 只能保证可见性和有序性,但不能保证原子性。比如 count++ 这种复合操作,它就搞不定,因为它没法保证 “读 - 改 - 写” 这三步不被打断。而 synchronized 是互斥锁,它能保证整个代码块的原子性,同一时间只有一个线程能执行,所以 count++ 用 synchronized 修饰就能保证结果正确。
其次,性能与场景不同。volatile 是轻量级同步,不涉及线程阻塞,性能很高,适合做简单的标志位(比如停止标记 isRunning)或者双重检查锁(DCL)。synchronized 是重量级同步,涉及操作系统的线程切换,开销大,但功能最全面,适合复杂的复合操作和多线程协作。
总结一句人话:如果是单个变量的赋值 / 读写,或者是防止指令重排序,用 volatile;如果是多步操作(比如 count++)或者复杂逻辑,必须用 synchronize
💡 提醒
- 讲到
count++时,一定要强调:**volatile**只能保证 “看到最新”,但保证不了 “同时操作”。 - 讲到指令重排序时,回扣你的单例模式 DCL 写法:
**volatile**就是为了解决 DCL 中**instance = new ...**的指令重排序问题。 - 对比时,直接抛出结论:
**volatile**是解决可见性和有序性的轻量级方案,**synchronized**是解决原子性的重量级方案。
更多推荐




所有评论(0)