CAS 的“ABA 问题”到底是个啥?看懂“前女友”的故事你就懂了
CAS 是一条 CPU 原子指令(cmpxchg它的核心思想是乐观锁。它在修改数据时,不加锁,而是抱着一种“赌徒”心态:“我猜根本没人跟我抢,我现在就去改。如果真有人抢了,我再重试。CAS 检查的是:“现在的值是不是 100?如果是,它就认为“没变过”。原来的值是A (100)。中间有个线程把它改成了B (50)。又有一个线程把它改回了A (100)。你的 CAS 过来检查:“哟,还是 100,没

在 Java 并发编程中,我们经常面临一个选择:
如果想让一个整数 i 安全地加 1,该怎么办?
-
方案 A (悲观派): 使用
synchronized锁住代码块。 -
后果: 安全,但太重了。就像为了防止有人插队,把整个售票厅大门都锁上,一个人办完业务才放下一个。
-
方案 B (乐观派): 使用
AtomicInteger。 -
后果: 飞快!它没有用锁,却保证了线程安全。
这就引出了 Java 并发包(J.U.C)的基石——CAS (Compare And Swap)。
💻 一、技术分析:一种“赌徒”心态
1. 什么是 CAS?
CAS 是一条 CPU 原子指令(cmpxchg)。它的核心思想是乐观锁。
它在修改数据时,不加锁,而是抱着一种“赌徒”心态:“我猜根本没人跟我抢,我现在就去改。如果真有人抢了,我再重试。”
2. 三个关键操作数
CAS 指令执行时,需要三个参数:
- V (Memory Value): 内存里真正存的值(比如现在的余额 100)。
- A (Expected Old Value): 我以为的值(旧预期值,我也以为是 100)。
- B (New Value): 我想修改成的新值(比如 110)。
操作逻辑:
- CPU 去检查:“内存里的 V 等于 A 吗?”
- 如果相等 (V == A): 说明期间没人动过。好,把 V 更新成 B (110)。修改成功。
- 如果不等 (V != A): 说明有人插队改过了(V 变成了 105)。修改失败,不许动内存。
3. 自旋 (Spinning)
如果修改失败了怎么办?放弃吗?
不。通常会配合一个**“死循环”**。
失败了 -> 重新读 V 的最新值 -> 重新计算 -> 再次尝试 CAS。
一直转圈圈,直到成功为止。这叫 自旋锁。
👔 二、故事场景:试衣间的“贴标签”游戏
为了搞懂 CAS 与 Synchronized 的区别,我们将 修改共享变量 比作 更衣室换衣服。
1. Synchronized —— “上锁的更衣室”
-
场景: 只有一个更衣室,门上有把大锁。
-
流程:
-
张三进去了,“咔嚓” 把门反锁。
-
李四、王五来了,推门推不开,只能在门口排队睡觉(线程阻塞)。
-
张三换完出来,叫醒李四。
-
评价: 悲观锁。认为总有人会冲进来,所以必须先锁门。线程切换成本高(叫醒服务很贵)。
2. CAS —— “开放式贴标签”
-
场景: 这里的规则是,衣服挂在墙上,大家都可以拿,但要给衣服贴上新价格。
-
流程:
-
第一步 (Read): 张三看到墙上的衣服标价是 100 元。
-
第二步 (Calculate): 张三心里想:“我要把它改成 110 元。”
-
第三步 (CAS 核心): 张三拿着“110”的标签冲上去,贴之前最后确认一眼:“现在的价格还是 100 吗?”
-
情况 A: 还是 100。啪!贴上 110。成功。
-
情况 B: 居然变成了 105(被李四抢先改了)。张三不贴了。
-
第四步 (Spin): 张三没放弃。他看了一眼现在的 105,重新计算:“那我改成 115 吧。” 再次冲上去尝试。
-
评价: 乐观锁。全程没有锁门,大家都在飞快地跑。只要没人抢,速度极快。
😈 三、致命缺陷:ABA 问题(前女友陷阱)
CAS 看起来很完美,但它有一个著名的逻辑漏洞。
1. 什么是 ABA?
CAS 检查的是:“现在的值是不是 100?”
如果 是,它就认为“没变过”。
但有没有一种可能:
- 原来的值是 A (100)。
- 中间有个线程把它改成了 B (50)。
- 又有一个线程把它改回了 A (100)。
- 你的 CAS 过来检查:“哟,还是 100,没人动过!” -> 修改成功。
虽然值没变,但“经历”变了。 这在某些场景下是致命的(比如栈结构、内存回收)。
2. 故事比喻:那杯水被喝过吗?
-
场景: 你桌上放了一杯满的水 (A)。你离开了一会儿。
-
过程:
-
你的室友过来,把水喝光了 (A -> B)。
-
他觉得不好意思,又接了一杯自来水放回去 (B -> A)。
-
结果: 你回来了。你用 CAS 眼神确认:“水还是满的 (A)”。于是你端起来喝了。
-
问题: 你以为是原来的纯净水,其实是自来水。你被骗了。
3. 解决方案:加版本号 (AtomicStampedReference)
怎么解决?给水杯贴个封条(版本号)。
- 原来是
100 (v1)。 - 改成 50 后变成
50 (v2)。 - 改回 100 后变成
100 (v3)。 - 你的 CAS 检查时:不仅要求值是 100,还要求版本号是 v1。v1 != v3,由于版本对不上,修改失败。
🎯 四、总结:无锁虽好,由于 CPU 烫手
CAS 是 Java 高性能并发(J.U.C 包)的基石,但它不是银弹。
- 优点: 快。没有线程阻塞和上下文切换的开销。
- 缺点:
- CPU 开销大: 如果竞争太激烈,张三一直在“自旋”重试,CPU 会被空转跑满(风扇狂转)。
- ABA 问题: 需要用版本号解决。
使用建议:
- 并发量不高(冲突少) -> 用 CAS (
Atomic类)。 - 并发量极高(冲突多,一直自旋不划算) -> 还是乖乖用 Synchronized 吧。
更多推荐



所有评论(0)