从一次"明明改了标志位,线程却停不下来"的诡异bug出发,深度剖析volatile关键字的三大核心作用:保证可见性、禁止指令重排序、以及单次读写的原子性。通过CPU缓存一致性协议的底层原理、双重检查锁(DCL)单例模式的经典案例、以及为什么volatile无法保证i++的原子性,揭秘volatile的使用场景与边界。配合内存屏障的图解和真实代码示例,给出volatile的最佳实践与常见误区。
一、一个停不下来的线程
周五下午,哈吉米跑过来:"哥,后台任务停不下来,点停止按钮没反应,你看看?"
我看了眼代码,逻辑很简单:
public class TaskRunner {
private boolean running = true; // 标志位
public void start() {
new Thread(() -> {
while (running) { // 根据标志位循环
// 处理任务
System.out.println("任务执行中...");
}
System.out.println("任务已停止");
}).start();
}
public void stop() {
running = false; // 改标志位
System.out.println("已设置停止标志");
}
}
哈吉米演示了一遍:
- 调用
start(),任务开始执行 - 调用
stop(),控制台输出"已设置停止标志" - 但任务还在跑,"任务执行中..."一直打印
- 完全停不下来
我当时懵了:明明已经把running改成false了,为什么while还在循环?
南北绿豆路过,插了一句:"你确定主线程改了,工作线程能看到吗?"
一语惊醒梦中人。调试发现,主线程里running确实是false,但工作线程里running还是true,两个线程看到的值不一样。
我加了一行volatile:
private volatile boolean running = true; // 加上volatile
哈吉米再测,秒停止。"卧槽,这么神奇?"
这就是可见性问题。
二、可见性:CPU缓存导致的"幻觉"
为什么会看到不同的值?
阿西噶阿西凑过来看热闹:"这是CPU缓存的锅。"
现代CPU为了提高性能,每个核心都有自己的缓存:
CPU核心1 → L1缓存 → L2缓存 ↘
L3缓存(共享) → 主内存
CPU核心2 → L1缓存 → L2缓存 ↗
问题来了:
- 主线程(CPU核心1)把running改成false,先写到L1缓存
- 工作线程(CPU核心2)从自己的L1缓存读running,还是true
- 两个线程各看各的缓存,值不同步
南北绿豆画了个图:
sequenceDiagram
participant M as 主线程(核心1)
participant C1 as L1缓存1
participant MM as 主内存
participant C2 as L1缓存2
participant W as 工作线程(核心2)
Note over M,C1: 初始:running=true
M->>C1: 1.写入running=false
Note over C1: running=false (缓存)
Note over W,C2: 同时在执行
W->>C2: 2.读取running
C2->>W: 3.返回true (旧值)
Note over W: 💀 while(running)继续循环
Note over M,W: 两个线程看到的值不一样
哈吉米:"所以缓存没同步,就出问题了?"
对,就是这样。
volatile怎么解决的?
加上volatile后,JVM会插入内存屏障(Memory Barrier),强制做两件事:
- 写操作:写完立即刷新到主内存,并通知其他CPU缓存失效
- 读操作:每次都从主内存读取最新值,不用缓存
sequenceDiagram
participant M as 主线程
participant MM as 主内存
participant W as 工作线程
M->>MM: 1.写入running=false(立即刷新到主内存)
MM->>W: 2.通知缓存失效
W->>MM: 3.从主内存读取
MM->>W: 4.返回false(最新值)
Note over W: ✅ while(running)停止循环
阿西噶阿西补充:"底层是CPU的lock指令加MESI缓存一致性协议,保证所有CPU看到的都是最新值。"
哈吉米:"听不懂,但很牛逼。"
三、有序性:指令重排序的坑
什么是指令重排序?
午饭时,南北绿豆问:"你们知道单例模式的双重检查锁吗?不加volatile会死人的。"
哈吉米:"知道啊,synchronized加两层检查,防止多次创建。"
南北绿豆:"那为什么instance要加volatile?"
哈吉米:"呃...不知道。"
南北绿豆开始讲课:"因为指令重排序。"
为了优化性能,编译器和CPU会重排指令:
// 原始代码
int a = 1; // 语句1
int b = 2; // 语句2
int c = a + b; // 语句3
// CPU可能重排成
int b = 2; // 语句2先执行
int a = 1; // 语句1后执行
int c = a + b; // 语句3
单线程没问题(结果不变),但多线程就炸了。
经典案例:双重检查锁(DCL)
单例模式的DCL写法,不加volatile会出问题:
public class Singleton {
private static Singleton instance; // 没有volatile,有问题
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题在这
}
}
}
return instance;
}
}
哈吉米:"看起来没问题啊。"
南北绿豆:"问题大了。"
问题出在哪?
new Singleton()在JVM里分三步:
// JVM指令(简化版)
memory = allocate(); // 1.分配内存空间
ctorInstance(memory); // 2.初始化对象
instance = memory; // 3.instance指向内存地址
CPU可能重排成:
memory = allocate(); // 1.分配内存
instance = memory; // 3.先赋值(对象还没初始化!)
ctorInstance(memory); // 2.后初始化
阿西噶阿西补充:"并发场景下的灾难:"
sequenceDiagram
participant T1 as 线程1
participant T2 as 线程2
participant I as instance
T1->>I: 1.分配内存
T1->>I: 2.instance=memory(未初始化)
Note over T2: 线程2进来了
T2->>I: 3.检查instance!=null
T2->>T2: 4.直接返回instance
Note over T2: 💀 拿到半初始化的对象,空指针或脏数据
T1->>I: 5.初始化对象(太晚了)
哈吉米:"卧槽,线程2拿到的是半成品?"
对,可能字段都是null,用起来就爆炸。
加volatile修复
private static volatile Singleton instance; // 加volatile
南北绿豆:"volatile禁止指令重排序,保证顺序:1.分配内存 → 2.初始化对象 → 3.赋值给instance,线程2拿到的一定是完整对象。"
哈吉米:"懂了懂了。"
四、volatile不保证原子性
下午,哈吉米又来了:"哥,我照着你说的加了volatile,怎么计数器还是不准?"
看代码:
public class Counter {
private volatile int count = 0; // 加了volatile
public void increment() {
count++; // 💀 不是原子操作
}
}
// 10个线程,每个+1000次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
}).start();
}
// 预期:10000
// 实际:9856(每次都不一样,总是小于10000)
哈吉米:"不是说volatile能保证可见性吗?怎么还丢数据?"
阿西噶阿西:"volatile不保证原子性啊,老弟。"
为什么volatile救不了i++?
count++在JVM里分三步:
// 伪代码
int temp = count; // 1.读取
temp = temp + 1; // 2.+1
count = temp; // 3.写回
南北绿豆:"volatile只保证每一步的可见性,但不保证三步合在一起是原子的:"
时间轴:
T1读取count=0
T2读取count=0 (T1还没写回,T2也读到0)
T1计算0+1=1
T2计算0+1=1
T1写回count=1
T2写回count=1 (覆盖了T1的结果)
预期:count=2
实际:count=1 (丢了一次+1)
哈吉米:"那怎么办?"
正确做法
// 方法1:AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // CAS原子操作
}
// 方法2:synchronized
private int count = 0;
public synchronized void increment() {
count++;
}
// 方法3:LongAdder(高并发推荐)
private LongAdder count = new LongAdder();
public void increment() {
count.increment();
}
阿西噶阿西:"推荐AtomicInteger或LongAdder,性能比synchronized好。"
哈吉米改完代码,测试通过:"完美!"
五、volatile的使用场景
晚上复盘,南北绿豆总结:
✅ 适合用volatile的场景
1. 状态标志
private volatile boolean shutdown = false;
// 线程1
while (!shutdown) {
// 处理任务
}
// 线程2
public void stop() {
shutdown = true; // 立即可见
}
南北绿豆:"就像今天早上那个bug,标志位最适合。"
2. 双重检查锁(DCL)
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 禁止重排序
}
}
}
return instance;
}
3. 独立观察
private volatile long lastUpdateTime;
// 线程1:更新时间
public void updateTime() {
lastUpdateTime = System.currentTimeMillis();
}
// 线程2:读取时间
public long getLastUpdateTime() {
return lastUpdateTime; // 总是读到最新值
}
❌ 不适合用volatile的场景
1. 复合操作
// ❌ 错误
private volatile int count;
public void increment() {
count++; // 不是原子操作
}
// ✅ 正确
private AtomicInteger count;
public void increment() {
count.incrementAndGet();
}
哈吉米:"这个我今天踩过了。"
2. 依赖旧值的更新
// ❌ 错误
private volatile int value;
public void update() {
value = value * 2 + 1; // 依赖旧值,不安全
}
// ✅ 正确
private int value;
public synchronized void update() {
value = value * 2 + 1;
}
3. 多个变量之间的不变式
// ❌ 错误
private volatile int lower = 0;
private volatile int upper = 10;
public void setRange(int lower, int upper) {
this.lower = lower; // 两个赋值不是原子的
this.upper = upper; // 中间状态可能lower>upper
}
// ✅ 正确
private int lower = 0;
private int upper = 10;
public synchronized void setRange(int lower, int upper) {
this.lower = lower;
this.upper = upper;
}
六、总结与对比
阿西噶阿西最后总结:
volatile三大作用
| 作用 | 说明 | 底层实现 |
|---|---|---|
| 可见性 | 一个线程修改,其他线程立即看到 | 内存屏障+缓存一致性协议 |
| 有序性 | 禁止指令重排序 | 内存屏障 |
| 单次读写原子性 | 一次读/写操作是原子的 | CPU保证(64位JVM) |
volatile vs synchronized
| 对比项 | volatile | synchronized |
|---|---|---|
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 可见性 | ✅ 保证 | ✅ 保证 |
| 有序性 | ✅ 保证 | ✅ 保证 |
| 性能 | 🚀 高(无锁) | 🐢 低(加锁) |
| 使用场景 | 状态标志、DCL | 复合操作、临界区 |
南北绿豆:"记住一句话:volatile适合'一写多读'的简单场景,synchronized适合'多写'的复合操作。"
哈吉米:"如果不确定用哪个呢?"
阿西噶阿西:"就用synchronized,安全第一。"
七、常见面试题
第二天早会,南北绿豆突然袭击:
南北绿豆:"哈吉米,volatile能保证原子性吗?"
哈吉米:"不能。只保证单次读/写的原子性,不保证复合操作(如i++)的原子性。昨天刚踩过坑。"
南北绿豆:"阿西噶阿西,volatile和synchronized的区别?"
阿西噶阿西:"volatile轻量级,不阻塞线程,但只保证可见性和有序性;synchronized重量级,会阻塞,但保证原子性+可见性+有序性。"
南北绿豆:"为什么DCL要加volatile?"
哈吉米:"防止指令重排序,避免拿到半初始化的对象。"
南北绿豆:"volatile的底层实现?"
阿西噶阿西:"内存屏障+MESI缓存一致性协议,写时刷新主内存并通知其他缓存失效,读时从主内存读最新值。"
南北绿豆:"可以,都学会了。散会!"
参考资料:
- 《Java并发编程的艺术》- 方腾飞
- 《深入理解Java虚拟机》- 周志明
- Doug Lea - JSR 133 (Java Memory Model)