文章目录
-
- [一、先说结论:volatile 三大特性](#一、先说结论:volatile 三大特性)
- 二、可见性:改了立刻让人看见
- 三、有序性:禁止指令重排序
- [四、为什么 volatile 不保证原子性?](#四、为什么 volatile 不保证原子性?)
- [五、volatile 的底层原理:内存屏障](#五、volatile 的底层原理:内存屏障)
- [六、volatile 的使用场景](#六、volatile 的使用场景)
-
- 场景一:状态标志
- 场景二:双重检查锁定(DCL)
- [场景三:读多写少用 CAS 保护写](#场景三:读多写少用 CAS 保护写)
- [volatile 全景](#volatile 全景)
- 回答技巧与点评
面试问并发,volatile 几乎必问。多数人能说出"保证可见性",但追问"什么是指令重排序"、"volatile 怎么保证有序性"、"为什么 volatile 不能保证原子性",就答不上来了。
今天咱们把 volatile 的三大特性、底层原理和使用边界彻底讲透。
一、先说结论:volatile 三大特性
| 特性 | volatile | 无 volatile |
|---|---|---|
| 可见性 | ✅ 修改后其他线程立即可见 | ❌ 可能读旧值 |
| 有序性 | ✅ 禁止指令重排序 | ❌ 可能被重排序 |
| 原子性 | ❌ 不保证 | ❌ 不保证 |
一句话记住:volatile 是"立竿见影但不包原子"------改了立刻让人看见,但改到一半被打断它管不了。
二、可见性:改了立刻让人看见
问题场景: 没有 volatile 时,线程可能读到旧值:
java
class Flag {
boolean running = true; // ❌ 没有 volatile
void stop() { running = false; }
void work() {
while (running) { // 线程可能永远看不到 running = false 👈
// ...
}
}
}
原因: 每个线程有自己的工作内存(CPU 缓存),不会每次都从主存读取。
加了 volatile:
java
volatile boolean running = true; // ✅ 修改后立刻刷回主存,其他线程立刻看到 👈
生活类比: 没有 volatile 像用黑板通知------你擦了重写,别人看的是自己抄的旧笔记;有 volatile 像用广播通知------你一改,所有人立刻听到。
三、有序性:禁止指令重排序
什么是指令重排序? 编译器和 CPU 为了优化性能,可能调整指令执行顺序:
java
// 你写的代码
int a = 1; // 语句 1
int b = 2; // 语句 2
boolean flag = true; // 语句 3
// CPU 可能重排序为
boolean flag = true; // 先执行 3
int a = 1; // 再执行 1
int b = 2; // 最后执行 2
单线程没问题,多线程可能出大事:
java
// 双重检查锁定(DCL)的经典问题
class Singleton {
private static Singleton instance; // ❌ 没有 volatile
static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 👈 可能被重排序!
}
}
}
return instance;
}
}
new Singleton() 不是原子操作,分三步:
1. 分配内存空间
2. 初始化对象(调用构造方法)
3. 将 instance 指向内存地址
重排序可能导致 2 和 3 交换:
线程 A:1 → 3 → 2(先指向地址,再初始化)
线程 B:判断 instance != null → 使用了一个未初始化的对象!💥
加 volatile 禁止重排序:
java
private static volatile Singleton instance; // ✅ 禁止 2 和 3 重排序 👈
四、为什么 volatile 不保证原子性?
java
volatile int count = 0;
// 两个线程同时执行 count++
// count++ 不是原子操作:读 → 加 1 → 写回
时间线:
1. 线程 A 读 count = 0
2. 线程 B 读 count = 0(volatile 保证读到最新值,但此时还没人改)
3. 线程 A 计算 0 + 1 = 1,写回 count = 1
4. 线程 B 计算 0 + 1 = 1,写回 count = 1(覆盖了!)
5. 结果:count = 1,但预期是 2 👈
volatile 只保证"读到的值是最新的",不保证"读-改-写"的原子性。
生活类比: volatile 像白板------你一写别人立刻看到,但两个人同时写,后写的会覆盖前写的。
五、volatile 的底层原理:内存屏障
JVM 通过插入**内存屏障(Memory Barrier)**实现 volatile 语义:
volatile 写操作前:插入 StoreStore 屏障
volatile 写操作后:插入 StoreLoad 屏障
volatile 读操作后:插入 LoadLoad 屏障 + LoadStore 屏障
| 屏障类型 | 作用 |
|---|---|
| StoreStore | 确保 volatile 写之前的普通写已刷回主存 |
| StoreLoad | 确保 volatile 写对后续读可见 |
| LoadLoad | 确保 volatile 读之后的读不会重排到之前 |
| LoadStore | 确保 volatile 读之后的写不会重排到之前 |
最底层: x86 架构中,volatile 写会在后面加 lock addl $0x0, (%rsp) 指令,lock 前缀会锁缓存行 + 触发缓存一致性协议,确保其他 CPU 的缓存失效。
六、volatile 的使用场景
场景一:状态标志
java
volatile boolean shutdown; // ✅ 一个线程写,多个线程读
void shutdown() { shutdown = true; }
void work() { while (!shutdown) { /* ... */ } }
场景二:双重检查锁定(DCL)
java
class Singleton {
private static volatile Singleton instance; // ✅ 禁止重排序
static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
场景三:读多写少用 CAS 保护写
java
volatile int value;
int getValue() { return value; } // ✅ 无锁读,性能高
void increment() {
int current;
do {
current = value;
} while (!CAS(value, current, current + 1)); // CAS 保证原子写 👈
}
不适合的场景: count++(需要原子性)→ 用 AtomicInteger。
volatile 全景
volatile 全景
三大特性
├── 可见性 ── 修改后刷回主存,其他线程立刻看到
├── 有序性 ── 内存屏障禁止指令重排序
└── 原子性 ── ❌ 不保证(count++ 不安全)
底层原理
├── 内存屏障(Memory Barrier)
│ ├── StoreStore + StoreLoad(写后)
│ └── LoadLoad + LoadStore(读后)
└── x86:lock 前缀指令 + 缓存一致性协议
适用场景
├── 状态标志(一个写多个读)
├── DCL 单例(禁止重排序)
└── CAS + volatile(无锁读 + 原子写)
不适用场景
├── count++ → 用 AtomicInteger
├── 复合操作 → 用锁
└── 需要互斥 → 用 synchronized 或 Lock
口诀:volatile 保可见防重排,原子操作它不管,
内存屏障是原理,lock 指令刷缓存,
状态标志和 DCL,场景选对才安全。
回答技巧与点评
标准回答
volatile 保证可见性和有序性,但不保证原子性。可见性指 volatile 变量修改后立刻刷回主存,其他线程读取时从主存获取最新值。有序性指通过内存屏障禁止指令重排序,典型应用是双重检查锁定单例。volatile 不保证原子性,因为 count++ 等操作是"读-改-写"三步,volatile 只保证读到最新值,不保证三步不被打断。底层通过插入内存屏障实现,x86 架构使用 lock 前缀指令触发缓存一致性协议。
加分回答
- happens-before 关系:volatile 写 happens-before 后续对同一变量的 volatile 读。这是 JMM(Java 内存模型)对 volatile 语义的形式化定义------不仅保证可见性,还建立了跨线程的 happens-before 关系链,是并发正确性推理的基础
- volatile 和 synchronized 的取舍:volatile 是"轻量级同步",只保证可见性和有序性,不加锁,性能好。synchronized 是"重量级同步",保证可见性、有序性和原子性,加锁,有性能开销。读多写少且写操作简单的场景用 volatile,复合操作用 synchronized
- LongAdder 的优化:Java 8 的 LongAdder 用"分段 CAS + volatile"替代了 AtomicLong 的"单一 CAS",在高并发写场景下性能更好------每个 Cell 独立 CAS,最终求和时读所有 Cell 的 volatile value。这是 volatile + CAS 的经典组合
面试官点评
这道题考的是你对 Java 内存模型和轻量级同步机制的理解。能说出"可见性+有序性,不保证原子性"是基本要求,能解释 DCL 重排序问题、内存屏障原理、以及 volatile vs synchronized 的取舍,才算及格。如果你能提到 happens-before 关系、LongAdder 的分段优化,面试官会认为你对 JMM 有系统性的理解,而不是只停留在 API 层面。
内容有帮助?点赞、收藏、关注三连!评论区等你 💪