程序崩了。查了三天,竟是因为 CPU 的一次误判。说实话,我当时想砸电脑。ABA 问题——这个教科书里轻描淡写的东西,真落到生产环境,能让你脱层皮。
你也许背过概念:线程1从内存位置 V 读出值 A,线程2把 V 改成 B 又改回 A,然后线程1用 CAS 比较 V 的当前值跟原先读出的 A 相等,于是交换成功。问题就在这里,最后一次比较时,值虽然还是 A,但“此 A”已非“彼 A”。链表里,它可能指向一个已经释放又被重新分配的节点,内容全变了。
CAS 的致命幻觉
CAS(Compare And Swap)本身是个原子操作,X86 有 CMPXCHG 指令。它会用硬件锁住总线,保证“比较-交换”不可分割。但 ABA 问题恰恰钻了原子的空子。打个比方:你停车时扫了一眼车牌是“京A88888”,然后去缴费。回来时发现还是那辆车,于是放心地开走。殊不知,这期间车被调包了——原车开走,另一辆外观一模一样的车停了进来,连车牌都是假的。你比较了“外观和车牌”,相等,但车上的东西已经不一样了。
在无锁栈的 pop 操作里,栈顶节点地址就是那个“车牌”。线程 T1 读到栈顶指针 ptrA,指向节点 N1。然后 T1 被抢占。T2 连续 pop 并释放 N1,接着 push 新节点恰好分配到 N1 的地址(内存分配器重用了这块内存),新节点记为 N2,地址还是 ptrA,但数据不同了。T1 恢复后执行 CAS,比较栈顶指针仍为 ptrA,认为栈没变,于是将栈顶更新为 N1 的下一个节点——这可就乱了,因为 N1 早已不是之前的节点,其 next 指针可能已经被覆盖成无效数据。
ABA问题内存变化栈示意图
看,这就是典型的“幻觉”。你那可怜的单次 CAS,根本感知不到中间那番颠鸾倒凤。