面试必问:volatile变量i++,两个线程各执行100次,结果一定是200吗?

🤔 先看一个「反直觉」的实验

假设我们有一个volatile int i = 0,启动两个线程,每个线程执行100次i++,最终i的值会是200吗?
答案:不一定,大概率小于200!
比如可能是198、199,但永远不会超过200。这是为什么呢?

🔍 揭开i++的「三步魔法」

看似简单的i++,背后藏着3个步骤(以线程A为例):

  1. 读取:从内存中读取i的当前值(比如0)
  2. 修改:在CPU寄存器中执行+1操作(变成1)
  3. 写入:将新值写回内存

如果两个线程同时执行这3步,就会发生「指令交错」,比如:

  • 线程A和线程B同时读取到i=0
  • 线程A修改为1,还没写入时,线程B也修改为1
  • 两者先后写入,最终i=1(相当于只执行了1次++,而不是2次)

💡 volatile的「两面性」

我们知道volatile有两个作用:

  1. 可见性:保证一个线程修改后,其他线程立即看到新值
  2. 禁止指令重排序:防止编译器/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到底是怎么实现无锁编程的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值