五分钟背一个面试题:Java 的 volatile 关键字
题目
请介绍一下 Java 中的 volatile 关键字,它的作用是什么?能保证原子性吗?和 synchronized 有什么区别?
参考答案
前言
"请介绍一下 volatile 关键字"——这道题在面试中的出现频率,大概跟早高峰地铁的拥挤程度差不多。
很多同学一听到 volatile,脑子里蹦出的第一个词是"可见性",然后就卡壳了。再问"它能保证原子性吗?""它和 synchronized 有啥区别?"——直接进入"阿巴阿巴"模式。
别慌。读完这篇文章,你不仅能把这几个问题讲得条理清晰,还能顺便甩出一个面试官大概率没听过的小细节,让他在心里给你默默加分。
一、volatile 到底是什么?——从一个翻车现场说起
假设你和室友共用一张便利贴,上面写着一个数字。你天天盯着便利贴看,想等数字变成 2 就出门买菜。结果你的眼睛像装了缓存似的——便利贴上明明已经写了 2,你眼里还是旧数据 1,于是你饿了一整天。
这就是多线程编程中的可见性问题。JVM 为了提高性能,允许每个线程把共享变量的副本缓存到自己的"工作内存"里(比如 CPU 寄存器或缓存),改的是副本,读的也是副本。什么时候同步回主内存?——看心情。
volatile 就是来解决这个问题的。被 volatile 修饰的变量,JVM 会跟你保证两件事:
- 写操作完成后,立即刷回主内存(不等了,当场就写)
- 读操作每次都去主内存拿最新值(不读缓存副本了)
用代码演一下:
// 不加 volatile
class WithoutVolatile {
private boolean flag = false;
public void writer() {
flag = true; // 写进工作内存,什么时候同步到主内存?不确定
}
public void reader() {
while (!flag) { // 可能永远读不到最新值,死循环!
// 线程:不是说好改了吗?我咋看不见?
}
System.out.println("终于看到了!");
}
}
// 加上 volatile
class WithVolatile {
private volatile boolean flag = false;
public void writer() {
flag = true; // 写完立刻刷回主内存,一秒都不拖
}
public void reader() {
while (!flag) { // 每次读都去主内存拿,保证是最新的
}
System.out.println("看到了!去买菜!");
}
}
你可以这样记:不加 volatile,线程就像戴着墨镜看世界,看到的永远是旧的。加了 volatile,就是摘掉墨镜,看到什么就是什么。
二、JMM(Java 内存模型)视角——硬核但不吓人
要彻底理解 volatile,绕不开 JMM。别被这三个字母吓到,我们用一个图搞定:
- 普通变量:线程A的工作内存 → 主内存(不知道啥时候同步),线程B的工作内存 → 主内存(不知道啥时候去取)
- volatile 变量:线程A写 → 立刻同步主内存;线程B读 → 每次都从主内存取
这个"立刻同步"的能力,就叫可见性(Visibility)。
三、禁止指令重排序——volatile 的第二项绝活
除了可见性,volatile 还有一个更隐蔽的能力:禁止指令重排序。
什么叫指令重排序?看这个例子:
// 你写的代码
int a = 1; // 语句1
int b = 2; // 语句2
flag = true; // 语句3
JVM 和 CPU 为了跑得更快,可能会把你的代码顺序打乱执行——比如先执行语句3,再执行语句1和2。在单线程下这完全没问题(反正最终结果一样),但在多线程下就可能翻车:
// 经典的双重检查锁定(DCL)单例——有 Bug 的版本
public class Singleton {
private static Singleton instance; // 没加 volatile!
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // ★ 这里出问题
}
}
}
return instance;
}
}
instance = new Singleton() 这一行代码,在 JVM 眼里其实是三步操作:
- 分配内存空间
- 调用构造函数初始化对象
- 将 instance 引用指向分配的内存地址
问题来了——步骤2和步骤3可能被重排序!也就是说,可能先执行3再执行2。如果线程A在步骤3执行完后(此时 instance 不为 null 但还没初始化完),线程B刚好执行到第一次 if (instance == null) 检查——它发现 instance 不为 null,直接返回了一个半成品对象,然后程序就崩了。
解法很简单,给 instance 加上 volatile:
private static volatile Singleton instance; // ✅ 禁止指令重排序
volatile 会在写操作前后插入内存屏障(Memory Barrier),相当于写了一堵墙——墙两边的指令,休想跨过墙重排序。
用个不太恰当但好记的比喻:volatile 就像高速上的隔离带,你可以在自己的车道里随便变道,但别想跨过隔离带去对面车道逆行。
四、最容易被问趴的地方——volatile 不能保证原子性
"volatile 能保证原子性吗?"
正确答案:不能。
这是面试官最爱挖的坑。很多人觉得 volatile 又能保证可见性、又能禁止重排序,那是不是就万事大吉了?那可不行。
看这个经典的反例:
public class Counter {
private volatile int count = 0;
public void increment() {
count++; // 你以为这是安全的自增?错!
}
}
count++ 看起来是一步操作,实际上是三步:
- 读:从主内存读取 count 的当前值
- 改:在 CPU 里把值 +1
- 写:把新值写回主内存
volatile 只保证了第1步读到的是最新值和第3步立刻写回,但在第1步和第3步之间,另一个线程可能已经读到了同样的旧值并且也执行了 +1。最终结果就是两次自增只加了一次。
这就是著名的非原子操作下的竞态条件。
| 时间 | 线程A | 线程B | count 主内存 |
|---|---|---|---|
| T1 | 读取 count=0 | — | 0 |
| T2 | — | 读取 count=0 | 0 |
| T3 | count=0+1=1 | — | 1(A刷回) |
| T4 | — | count=0+1=1 | 1(B也刷回,但应该是2!) |
要解决这个问题,你需要真正的原子操作——synchronized、AtomicInteger 或者 Lock:
// 方案一:synchronized
public synchronized void increment() { count++; }
// 方案二:AtomicInteger(底层用 CAS,无锁更高效)
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
一句话总结:volatile 管得了视线(可见性),管不了手速(原子性)。
五、volatile vs synchronized —— 终极对比
面试官问完 volatile,大概率会跟一句:"那它和 synchronized 有什么区别?"这是送分题,也是一个展示你理解深度的好机会。
| 对比维度 | volatile | synchronized |
|---|---|---|
| 本质 | 轻量级同步机制 | 重量级锁机制 |
| 修饰对象 | 只能修饰变量 | 修饰方法、代码块 |
| 保证可见性 | ✅ 是 | ✅ 是 |
| 禁止重排序 | ✅ 是(部分) | ✅ 是 |
| 保证原子性 | ❌ 不能 | ✅ 能 |
| 阻塞线程 | ❌ 不阻塞 | ✅ 会阻塞 |
| 性能开销 | 很低(无上下文切换) | 较高(涉及锁竞争和线程调度) |
| 适用场景 | 状态标志、DCL 单例 | 复合操作、临界区保护 |
用一句人话来记:
volatile 就像一个"喊话的人"——它能让你听到最新消息(可见性),但管不了你听到了之后怎么做(原子性)。synchronized 则像"交通警察"——不仅能让你看到最新路况,还管着你什么时候能走、走哪条道。
六、volatile 的典型使用场景
不是所有变量都应该加 volatile。下面的场景才是它发光发热的地方:
场景1:状态标志位
public class Worker implements Runnable {
private volatile boolean running = true; // ★ volatile
public void run() {
while (running) {
// 干活中...
}
System.out.println("收到停工通知,下班!");
}
public void stop() {
running = false; // 主线程一改,Worker 线程立刻感知
}
}
这种场景用 volatile 简直完美——只涉及单次读写,不存在竞态条件。
场景2:双重检查锁定(DCL)单例模式
前面已经分析过了,这是 volatile 最经典的复杂场景。
public class Singleton {
private static volatile Singleton instance; // ★ 必须加!
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
场景3:CAS 中的变量
AtomicInteger、AtomicReference 等原子类的内部 value 字段,就是用 volatile 修饰的:
// AtomicInteger 源码片段
private volatile int value;
CAS 保证了原子性(Compare and Swap),volatile 保证了每次 CAS 操作读到的是最新值。
七、常见面试追问
Q1:volatile 能保证线程安全吗?
不能一概而论。 volatile 只能保证可见性和禁止指令重排序,不能保证原子性。如果操作本身是原子的(比如对 boolean 或 int 的简单赋值),那 volatile 就够了。如果涉及复合操作(如 i++),还需要加锁或用原子类。
Q2:所有变量加上 volatile 不就好了?为啥不默认加?
两个原因:
- 性能开销:volatile 的读写直接从主内存走,绕过了 CPU 缓存,比普通变量慢不少。所有变量都加 volatile,程序性能会大打折扣。
- 大部分场景不需要:单线程里根本用不着,多线程里很多时候有 synchronized 或 Lock 已经搞定了。
Q3:volatile 和 AtomicInteger 选哪个?
- 只是简单的读写状态标记(比如开关)→ volatile
- 需要
i++等复合操作 → AtomicInteger(CAS 实现,无锁高效) - 需要多步操作的原子性(比如先检查再操作)→ synchronized 或 Lock
Q4:volatile 变量的自增操作用 volatile 能保证正确吗?
不能,这就是前面第四节的翻车现场。i++ 是读-改-写三步,volatile 只保证每步读到最新值,但不能阻止别人在你读和写之间也读。正确姿势是用 AtomicInteger.incrementAndGet()。
八、一句话总结
volatile 是 Java 最轻量的同步机制——保证可见性,禁止指令重排,但不保证原子性。线程安全的及格线是 volatile,满分还得靠锁或原子类。
面试口语回答模版
面试官:"请介绍一下 volatile 关键字。"
你可以这样答:
volatile 是 Java 提供的一种轻量级同步机制。它主要有两个作用:
第一,保证可见性。被 volatile 修饰的变量,一个线程修改后,其他线程能立即看到最新值。这是因为 volatile 要求写操作立刻刷回主内存,读操作每次都从主内存取。
第二,禁止指令重排序。JVM 和 CPU 为了优化可能会打乱指令执行顺序,volatile 通过内存屏障来防止这种重排序。最典型的应用场景就是双重检查锁定的单例模式,如果 instance 不加 volatile,可能因为指令重排导致别的线程拿到一个还没初始化完的对象。
但是,volatile 不能保证原子性。像
i++这种复合操作,volatile 是管不了的,需要用 synchronized 或者 AtomicInteger 来解决。和 synchronized 相比,volatile 是更轻量的,不会引起线程阻塞和上下文切换,但它只能修饰变量,synchronized 可以修饰方法和代码块,还能保证原子性。
实际开发中,volatile 适合用在状态标志位、DCL 单例等场景。
更多推荐



所有评论(0)