核心机制
volatile关键字通过两个底层机制解决多线程可见性问题:
- 强制刷新与读取主内存:保证可见性
- 插入内存屏障:禁止指令重排序
一、保证可见性的原理
1. JMM内存模型
线程A工作内存 主内存 线程B工作内存
┌─────────┐ ┌─────────┐
│ flag拷贝 │ │ flag拷贝 │
│ (false) │ │ (false) │
└─────────┘ └─────────┘
↑ ↑ ↑
└───────────────┴───────────────┘
volatile flag = false
问题:线程A修改flag,可能只更新工作内存,线程B看不到。
volatile解决:
volatile boolean flag = false;
// 线程A写入
flag = true;
// ↓ volatile写:立即刷新到主内存,并使其他CPU缓存失效
// 线程B读取
if (flag) {
// ↓ volatile读:强制从主内存读取最新值
}
2. 硬件层面(MESI协议)
CPU0 主内存 CPU1
Cache: flag=false ←→ flag=false ←→ Cache: flag=false
↓ (写入true)
Cache: flag=true (Modified)
↓ 发送Invalidate消息
Cache: flag=? (Invalid)
↓ 读取flag
从主内存重新加载: flag=true
MESI状态:
- M (Modified):当前CPU独占且已修改
- E (Exclusive):当前CPU独占但未修改
- S (Shared):多个CPU共享
- I (Invalid):缓存无效
二、禁止重排序的原理
1. 内存屏障的插入策略
class VolatileBarriers {
int data = 0;
volatile boolean ready = false;
// 写线程
public void writer() {
data = 42; // 普通写
// ─────────────────────────
// StoreStore屏障
// ─────────────────────────
ready = true; // volatile写
// ─────────────────────────
// StoreLoad屏障(最强,最耗性能)
// ─────────────────────────
}
// 读线程
public void reader() {
// ─────────────────────────
// LoadLoad屏障
// ─────────────────────────
if (ready) { // volatile读
// ─────────────────────────
// LoadStore屏障
// ─────────────────────────
System.out.println(data); // 保证看到42
}
}
}
2. happens-before语义
// volatile的happens-before规则
volatile int v = 0;
int a = 0;
// 线程A
a = 1; // 操作1
v = 1; // 操作2(volatile写)
// 线程B
if (v == 1) { // 操作3(volatile读)
int b = a; // 操作4,保证b=1
}
// happens-before链:
// 1 happens-before 2(程序顺序规则)
// 2 happens-before 3(volatile规则)
// 3 happens-before 4(程序顺序规则)
// 传递性:1 happens-before 4
三、综合案例
案例1:双重检查锁单例
public class Singleton {
// volatile保证:可见性 + 禁止重排序
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 读1(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 读2(加锁)
instance = new Singleton(); // 写
}
}
}
return instance;
}
}
volatile的双重作用:
- 禁止重排序: ```java // 对象创建的三步: memory = allocate(); // 1. 分配内存 ctorInstance(memory); // 2. 初始化 instance = memory; // 3. 赋值引用
// volatile禁止2和3重排序,避免返回半初始化对象
2. **保证可见性**:
```java
// 线程A创建对象后,volatile确保instance立即对其他线程可见
// 线程B读取instance时,能看到完全初始化的对象
案例2:生产者-消费者
public class ProducerConsumer {
private String data;
private volatile boolean ready = false;
// 生产者
public void produce() {
data = "Hello World"; // 1. 准备数据
ready = true; // 2. volatile写
// StoreStore屏障确保1在2之前完成
}
// 消费者
public void consume() {
while (!ready) { // 3. volatile读
// 自旋等待
}
// LoadLoad屏障确保4在3之后执行
System.out.println(data); // 4. 使用数据,保证看到"Hello World"
}
}
案例3:状态标志
public class Server {
private volatile boolean running = true;
// 主线程
public void run() {
while (running) { // volatile读,保证及时看到shutdown的修改
// 处理请求
processRequest();
}
cleanup();
}
// 其他线程
public void shutdown() {
running = false; // volatile写,立即对run()线程可见
}
}
四、性能考量
开销对比
// JMH基准测试
public class VolatilePerformance {
int normalVar = 0;
volatile int volatileVar = 0;
AtomicInteger atomicVar = new AtomicInteger(0);
@Benchmark
public void testNormal() {
normalVar++; // ~1ns(非线程安全)
}
@Benchmark
public void testVolatile() {
volatileVar++; // ~10-20ns(可见性 + 重排序保护)
}
@Benchmark
public void testAtomic() {
atomicVar.incrementAndGet(); // ~20-50ns(原子性 + CAS自旋)
}
}
使用建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 状态标志 | volatile | 轻量级,无锁 |
| 计数器 | AtomicInteger | 保证原子性 |
| 单次发布对象 | volatile | DCL模式 |
| 复合操作 | synchronized | 保证原子性 |
五、常见误区
误区1:volatile保证原子性
// 错误
volatile int count = 0;
count++; // 非原子!包含"读-改-写"三步
// 正确
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
误区2:volatile万能
// 错误:多个变量无法保证一致性
volatile int x = 0;
volatile int y = 0;
x++;
y++; // 中间可能被其他线程打断
// 正确:使用锁
synchronized (lock) {
x++;
y++;
}
误区3:不需要volatile
// 错误:以为JVM会自动同步
boolean stop = false;
while (!stop) { ... } // 可能永远看不到修改
// 正确
volatile boolean stop = false;
答题总结
核心要点
- 可见性机制:
- volatile写:立即刷新到主内存
- volatile读:强制从主内存读取
- MESI协议使其他CPU缓存失效
- 禁止重排序:
- volatile写前插入StoreStore屏障
- volatile写后插入StoreLoad屏障
- volatile读后插入LoadLoad + LoadStore屏障
- happens-before:
- volatile写 happens-before volatile读
- 传递性保证前后操作的可见性
面试答题模板
“volatile通过两个机制解决多线程可见性和重排序问题。
可见性方面,volatile变量的写操作会立即刷新到主内存,读操作会强制从主内存加载。底层利用CPU的MESI缓存一致性协议,当一个CPU修改volatile变量时,会使其他CPU的缓存行失效,强制它们重新从主内存读取。
禁止重排序方面,JMM会在volatile操作前后插入内存屏障。volatile写之前插入StoreStore屏障,确保之前的普通写不会被重排序到volatile写之后;写之后插入StoreLoad屏障,确保volatile写不会与之后的读操作重排序。volatile读之后插入LoadLoad和LoadStore屏障,确保volatile读与后续操作不会重排序。
典型应用是双重检查锁的单例模式,volatile既保证instance的可见性,又禁止对象创建过程的重排序,避免返回半初始化对象。
但要注意volatile不保证原子性,像i++这种复合操作仍需要用AtomicInteger或synchronized。”