地址没变,但里面的东西已经换了一茬,CAS 被骗了。
打个比方
你桌上放了杯奶茶,你瞅了一眼:是珍珠奶茶
你转身去接个电话。
这 5 分钟里:
同事把奶茶喝完了,杯子扔了
保洁阿姨把杯子捡回来,又倒了杯新的珍珠奶茶进去
杯子还是那个杯子,位置也没变
你回来一看:哟,奶茶还在!
于是你放心地拿起来喝了一口。
但这是另一杯奶茶了。
对应到代码里
CAS 就是那个"瞅一眼"的操作:
// 你瞅了一眼:head 指向地址 A old = head.load(); // 你去干别的事了(被别的线程抢占了) // 这时候: // 别的线程把 A 弹走了 // 释放了 A 的内存 // 又分配了新节点,刚好分配到同一个地址 A // 你回来,CAS 检查:head 还是 A 吗? // 是!地址一样!CAS 成功! head.compare_exchange_strong(old, new_ptr);CAS 只认地址,不认"这个地址里装的是不是原来的东西"。
为什么叫 ABA
因为变化路径是:
A → B → A你开头看到 A,结尾看到 A,中间 B 发生过啥,CAS 根本不知道。
就像你早上看女朋友在沙发上,晚上看还在沙发上,中间她换了个灵魂你不知道。
为什么这么坑
因为 CAS 成功后,你拿着"旧记忆"去操作:
你以为
A->next还是原来的 B其实 A 里面的数据已经全换了
你基于错误的前提改结构,链表断、内存重复释放、读到别人的数据
而且这种 bug 特别阴间:
单线程跑:永远不出
低并发跑:几个月不出
高并发压测:随机崩,还不一定能复现
怎么治
1. 加版本号——给每个地址办个"身份证"
A:第1代 → B:第2代 → A:第3代你手上是 A 第 1 代,现在系统是 A 第 3 代,CAS 一眼识破。
2. 不让内存复用——别人还在用 A 的时候,谁都不许 delete A
等确认没人用了,再真正释放。地址永远不会"死而复生"。
3. 用句柄不用裸指针——发身份证,不发人脸
(槽位3,第5代)槽位 3 后来被复用,但代次变成 6,你拿第 5 代的票来用,系统直接拒。
一句话总结
CAS 是"认脸"的,ABA 就是:脸没变,魂换了。
版本号让它能认魂,Hazard Pointer 不让魂投胎,句柄直接不发人脸,发身份证。