面试必备:volatile 关键字深度解析
volatile 是什么?
volatile 是 Java 中的轻量级同步机制,用来修饰变量。
public class VolatileDemo {
volatile boolean flag = false; // volatile 修饰
public void writer() {
flag = true; // 写
}
public void reader() {
if (flag) { // 读
// 一定能读到最新值
}
}
}
两个核心特性
1. 可见性(Visibility)
问题场景:多线程下,一个线程修改了变量,其他线程看不到。
public class VisibilityProblem {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// 线程 A:不断检查 flag
new Thread(() -> {
while (!flag) { } // 可能永远不会停下来
System.out.println("线程 A 检测到 flag = true");
}).start();
// 线程 B:修改 flag
new Thread(() -> {
flag = true;
System.out.println("线程 B 修改 flag = true");
}).start();
}
}
原因:每个线程有自己的工作内存(CPU 缓存),对变量的读写先到缓存,不立刻写回主内存。
线程 A 的工作内存 线程 B 的工作内存 主内存
┌─────────────┐ ┌─────────────┐ ┌─────────┐
│ flag=true │ │ flag=false│ ←─→ │ flag= │
│ (缓存) │ │ (缓存) │ │ false │
└─────────────┘ └─────────────┘ └─────────┘
↑
线程 B 修改后,其他线程
可能看不到这个改动
volatile 保证:变量被修改后,立刻写回主内存,并通知其他线程的缓存失效。
volatile boolean flag = false; // 加 volatile 后,立即可见
2. 有序性(Ordering)
问题场景:编译器/CPU 可能对代码进行重排序。
// 线程 A
initialized = true; // 1. 初始化完成
writeRef(obj, instance); // 2. 写入引用
// 线程 B
if (initialized) { // 3. 检查是否初始化
use(obj); // 4. 使用对象
}
问题:如果重排序后,writeRef 先执行,线程 B 可能在对象还没初始化完成时就使用了它。
volatile 禁止重排序:
// volatile 写前后的指令不能重排
volatile boolean initialized = false;
initialized = true; // 这行之前的指令不能排到它后面
writeRef(obj, instance); // 这行之后的指令不能排到它前面
底层实现:内存屏障
什么是内存屏障?
CPU 用来禁止指令重排序和强制刷新缓存的机制。
volatile 的内存屏障
volatile 写操作:
┌────────────────────────────────────────┐
│ StoreStore 屏障 │
│ 对 volatile 变量的写入 │
│ StoreLoad 屏障 │
└────────────────────────────────────────┘
volatile 读操作:
┌────────────────────────────────────────┐
│ LoadLoad 屏障 │
│ 读取 volatile 变量 │
│ LoadStore 屏障 │
└────────────────────────────────────────┘
效果:
- volatile 写之前:前面的指令必须在写之前执行完
- volatile 写之后:后面的指令必须在写之后执行
- volatile 读之前:后面的指令必须在读之前执行完
- volatile 读之后:前面的指令必须在读之后执行
volatile vs synchronized
| 特性 | volatile | synchronized |
|---|---|---|
| 作用对象 | 变量 | 方法/代码块 |
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 性能 | 轻量级 | 重量级 |
volatile 不保证原子性:
volatile int count = 0;
// 两个线程各执行 10000 次 ++
count++; // ++ 操作不是原子的!
count++ 实际是三步操作:
- 读取 count
- count + 1
- 写回 count
两个线程可能同时读到相同的值,结果小于 20000。
volatile 的应用场景
1. 状态标志
volatile boolean running = true;
public void shutdown() {
running = false; // 通知线程停止
}
public void run() {
while (running) {
// 执行业务逻辑
}
}
2. 双重检查锁定(单例模式)
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;
}
}
为什么要 volatile?
instance = new Singleton() 实际是三步:
- 分配内存
- 调用构造函数
- 赋值给 instance
没有 volatile,步骤 2 和 3 可能重排序,导致其他线程拿到未构造完成的对象。
3. DCL(Double Checked Locking)原理
线程 A 线程 B
──────────────────────── ────────────────────────
instance = new Singleton()
↓
分配内存
↓ if (instance == null) // 可能是半初始化对象
instance = 地址A return instance ← 返回一个"看起来正常"的对象
↓ use(instance) ← 错误!对象还没构造完
构造对象
volatile 禁止了重排序,避免这个问题。
JMM(Java 内存模型)
volatile 的语义实际上是 JMM 的一部分:
JMM
┌───────────────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │Thread A │ │Thread B │ │
│ │工作内存 │ │工作内存 │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ └─────┬──────┘ │
│ │ │
│ ┌────▼────┐ │
│ │ 主内存 │ │
│ └─────────┘ │
│ │
└───────────────────────────┘
volatile 保证:
├─ 写 → 主内存(立即刷新)
└─ 读 → 从主内存读取(绕过缓存)
面试高频问题
Q1:volatile 是怎么保证可见性的?
通过写屏障(Store Barrier) 和读屏障(Load Barrier):
- 写:强制将值写入主内存
- 读:强制从主内存读取,并让其他线程的缓存失效
Q2:volatile 能保证原子性吗?
不能。volatile 只保证可见性和有序性,不保证复合操作(如 count++)的原子性。
Q3:volatile 和 synchronized 哪个性能好?
volatile 性能好(轻量级),但功能也更少。synchronized 功能强,但重量级。
Q4:volatile 修饰 long、double 有什么特殊?
- volatile 修饰 long/double:保证 64 位操作的原子性
- JVM 中,对 non-volatile long/double 的读写可能分成两个 32 位操作
- volatile 修饰后,保证读写是原子的
总结
volatile 的两个特性:
1. 可见性
└─ 修改后立即写主内存
└─ 读取时强制从主内存读
└─ 通知其他 CPU 缓存失效
2. 有序性
└─ 禁止指令重排序
└─ 依赖内存屏障实现
使用场景:
├─ 状态标志(boolean flag)
├─ 一次性安全发布(单例模式)
└─ 读取远多于写入的场景
注意:
├─ volatile 不保证原子性
├─ synchronized 保证原子性
└─ volatile 是轻量级,synchronized 是重量级