🤔 先看一个「反直觉」的实验
假设我们有一个volatile int i = 0
,启动两个线程,每个线程执行100次i++
,最终i
的值会是200吗?
答案:不一定,大概率小于200!
比如可能是198、199,但永远不会超过200。这是为什么呢?
🔍 揭开i++的「三步魔法」
看似简单的i++
,背后藏着3个步骤(以线程A为例):
- 读取:从内存中读取i的当前值(比如0)
- 修改:在CPU寄存器中执行+1操作(变成1)
- 写入:将新值写回内存
如果两个线程同时执行这3步,就会发生「指令交错」,比如:
- 线程A和线程B同时读取到i=0
- 线程A修改为1,还没写入时,线程B也修改为1
- 两者先后写入,最终i=1(相当于只执行了1次++,而不是2次)
💡 volatile的「两面性」
我们知道volatile
有两个作用:
- 可见性:保证一个线程修改后,其他线程立即看到新值
- 禁止指令重排序:防止编译器/CPU打乱指令顺序
但volatile不保证原子性!
原子性指「操作要么全做,要么不做」,而i++
是3步操作,中间可能被打断。
就像你去银行存钱,先查余额100元,取100元存进去,结果被人中途修改了余额,最终存款可能出错。
🚀 为什么结果不会超过200?
每个线程最多执行100次++,总操作次数是200次。
但由于「读取-修改-写入」的步骤可能被覆盖,导致部分操作无效(比如上面的例子丢失了一次增量),所以最终结果只会小于等于200。
只有当所有操作完全不冲突时,才会刚好等于200(但这种情况概率极低,几乎不可能)。
✅ 正确的做法:保证原子性
如果需要线程安全的计数,有两种方案:
1. 使用synchronized
同步块
private int i = 0;
private Object lock = new Object();
// 线程A和B执行这个方法
public void add() {
synchronized (lock) { // 同一时间只有一个线程进入
i++;
}
}
synchronized
保证「读取-修改-写入」三步作为整体执行,不会被打断。
2. 使用AtomicInteger
原子类
private AtomicInteger i = new AtomicInteger(0);
// 线程A和B执行这个方法
public void add() {
i.incrementAndGet(); // 原子操作,内部用CAS实现
}
AtomicInteger
底层通过CAS(compare-and-swap)机制保证原子性,效率比synchronized
更高。
📌 总结:volatile的「适用场景」
- ✔️ 正确场景:状态标记(如
boolean running = false
)、保证可见性的简单变量(如配置开关) - ❌ 错误场景:需要原子操作的场景(如i++、i–、复合操作)
记住:volatile是轻量级的可见性保障,不是线程安全的万能药!
🤝 课后小思考
如果把i++
换成i = i + 1
,结果会一样吗?为什么?
(答案:一样,因为本质还是3步操作,非原子性)
觉得有帮助的话,点赞收藏不迷路~ 下次聊聊CAS到底是怎么实现无锁编程的!