作者小头像 Lv.1
6 成长值

个人介绍

这个人很懒,什么都没有留下

感兴趣或擅长的领域

暂无数据
个人勋章
TA还没获得勋章~
成长雷达
0
6
0
0
0

个人资料

个人介绍

这个人很懒,什么都没有留下

感兴趣或擅长的领域

暂无数据

达成规则

发布时间 2026/10/21 15:52:15 最后回复 yd_265768724 2026/10/22 15:10:39 版块 开发者空间
30 5 0
他的回复:
一、CAS 为什么必然遇到 ABACAS(Compare-And-Swap)的语义是「如果当前值等于期望值,就替换成新值」,它只比较「值」,不关心「历史」。问题是:值相等不代表状态没变过。线程 1 读到 A 后被挂起,线程 2 把值改成 B 又改回 A,线程 1 恢复后 CAS(A→C) 比较依然成功——但中间两轮修改的副作用(节点被 free 后地址复用、计数器被清零又填回)全部被掩盖。只要满足「值会回到旧形态」+「中间态有副作用」两个条件,ABA 就必然发生,这不是实现 bug,是 CAS 语义的固有盲区。二、三种机制分别切哪个环节三者不是竞争关系,而是分别作用于 ABA 成因链条上的不同环节:比较环节、内存复用环节、原地修改环节。Tagged Pointer(版本号戳)——作用在「比较环节」,让比较带上历史把「指针/值 + 单调递增的版本号」打包成一个更宽的机器字(通常是 128 位)做 CAS。值即使从 B 变回 A,版本号也已经 +2,比较必然失败,ABA 在第一步就被识破。Java 的 AtomicStampedReference 就是这个思路(stamp 每次修改 +1),AtomicMarkableReference 用布尔标记位是它的简化版;C/C++ 需要 DW-CAS(双字 CAS)或手写 16 字节比较。代价是原子操作宽度翻倍,部分平台不支持,且 stamp 本身也有 wraparound 问题(理论极窄但存在)。2. Hazard Pointer——作用在「内存复用环节」,让危险节点不被回收ABA 的危害几乎都来自「节点被释放后地址复用」。Hazard Pointer 的思路是每个线程在解引用共享指针前,先把自己即将访问的节点地址发布到线程本地的 hazard 槽;其他线程要 free 节点时,先扫描所有 hazard 槽,发现有人声明了「我马上要碰这个节点」就推迟释放,等对方用完(移出 hazard 槽)后再在安全点回收。它不阻止值变回 A,而是直接消灭 ABA 的放大器——复用。适合无锁栈、队列这类读多写少的结构,开销在每次发布/扫描。3. RCU——作用在「原地修改环节」,干脆不做原地改Read-Copy-Update 是更宏观的策略:写者不原地修改共享数据,而是复制一份、改副本、最后原子地替换指针;读者全程无锁、无阻塞、不需要原子读。由于旧版本在「宽限期(grace period)」内不会被释放(要等所有读者退出临界区),地址复用同样被推迟。Linux 内核用它支撑海量读场景。代价是内存放大(同时存在多版本)和宽限期的等待延迟,适合「读远多于写、容忍短暂旧数据」的场景。三、怎么选如果只是普通计数器、序列号这类值单调递增不会回退的场景,天然无 ABA,三者都不需要。涉及裸指针的无锁结构(C/C++、共享内存):优先 Tagged Pointer 做第一道防线,配合 Hazard Pointer 或 RCU 管内存回收;Java 有 GC 且对象地址不会被重新分配给新对象,纯 JVM 场景 ABA 风险低,但用 unsafe 操作堆外内存时风险回归,该防还是要防。 
发布时间 2026/10/21 15:52:15 最后回复 yd_265768724 2026/10/22 15:10:39 版块 开发者空间
30 5 0
他的回复:
一、CAS 为什么必然遇到 ABACAS(Compare-And-Swap)的语义是「如果当前值等于期望值,就替换成新值」,它只比较「值」,不关心「历史」。问题是:值相等不代表状态没变过。线程 1 读到 A 后被挂起,线程 2 把值改成 B 又改回 A,线程 1 恢复后 CAS(A→C) 比较依然成功——但中间两轮修改的副作用(节点被 free 后地址复用、计数器被清零又填回)全部被掩盖。只要满足「值会回到旧形态」+「中间态有副作用」两个条件,ABA 就必然发生,这不是实现 bug,是 CAS 语义的固有盲区。二、三种机制分别切哪个环节三者不是竞争关系,而是分别作用于 ABA 成因链条上的不同环节:比较环节、内存复用环节、原地修改环节。Tagged Pointer(版本号戳)——作用在「比较环节」,让比较带上历史把「指针/值 + 单调递增的版本号」打包成一个更宽的机器字(通常是 128 位)做 CAS。值即使从 B 变回 A,版本号也已经 +2,比较必然失败,ABA 在第一步就被识破。Java 的 AtomicStampedReference 就是这个思路(stamp 每次修改 +1),AtomicMarkableReference 用布尔标记位是它的简化版;C/C++ 需要 DW-CAS(双字 CAS)或手写 16 字节比较。代价是原子操作宽度翻倍,部分平台不支持,且 stamp 本身也有 wraparound 问题(理论极窄但存在)。2. Hazard Pointer——作用在「内存复用环节」,让危险节点不被回收ABA 的危害几乎都来自「节点被释放后地址复用」。Hazard Pointer 的思路是每个线程在解引用共享指针前,先把自己即将访问的节点地址发布到线程本地的 hazard 槽;其他线程要 free 节点时,先扫描所有 hazard 槽,发现有人声明了「我马上要碰这个节点」就推迟释放,等对方用完(移出 hazard 槽)后再在安全点回收。它不阻止值变回 A,而是直接消灭 ABA 的放大器——复用。适合无锁栈、队列这类读多写少的结构,开销在每次发布/扫描。3. RCU——作用在「原地修改环节」,干脆不做原地改Read-Copy-Update 是更宏观的策略:写者不原地修改共享数据,而是复制一份、改副本、最后原子地替换指针;读者全程无锁、无阻塞、不需要原子读。由于旧版本在「宽限期(grace period)」内不会被释放(要等所有读者退出临界区),地址复用同样被推迟。Linux 内核用它支撑海量读场景。代价是内存放大(同时存在多版本)和宽限期的等待延迟,适合「读远多于写、容忍短暂旧数据」的场景。三、怎么选如果只是普通计数器、序列号这类值单调递增不会回退的场景,天然无 ABA,三者都不需要。涉及裸指针的无锁结构(C/C++、共享内存):优先 Tagged Pointer 做第一道防线,配合 Hazard Pointer 或 RCU 管内存回收;Java 有 GC 且对象地址不会被重新分配给新对象,纯 JVM 场景 ABA 风险低,但用 unsafe 操作堆外内存时风险回归,该防还是要防。