ABA问题

多线程 计算中ABA问题发生在同步期间,当一个位置被读取两次,两个读取具有相同的值,并且“值相同”用于指示“没有任何改变”。但是,另一个线程可以在两个读取之间执行并更改值,执行其他工作,然后更改该值,从而欺骗第一个线程思考“没有任何改变”,即使第二个线程的工作违反了该假设。

当访问共享数据的多个线程(或进程)交错时,会发生ABA问题。以下是导致ABA问题的事件序列:

  • 处理 {\ displaystyle P_ {1}}P_ {1} 从共享内存读取值A,
  • {\ displaystyle P_ {1}}P_ {1}抢占,允许进程{\ displaystyle P_ {2}}P_ {2} 跑步,
  • {\ displaystyle P_ {2}}P_ {2} 在抢占之前将共享内存值A修改为值B并返回到A,
  • {\ displaystyle P_ {1}}P_ {1} 再次开始执行,看到共享内存值没有改变并继续。

虽然 {\ displaystyle P_ {1}}P_ {1} 可以继续执行,由于共享内存中的“隐藏”修改,行为可能不正确。

在实现无数据结构时遇到ABA问题的常见情况。如果从列表中删除项目,删除,然后分配新项目并将其添加到列表中,由于优化,分配的对象通常与删除的对象位于同一位置。因此,指向新项目的指针有时等于指向旧项目的指针,这是ABA问题。

 

解决方法

标记状态参考

常见的解决方法是在正在考虑的数量上添加额外的“标记”或“标记”位。例如,对指针使用比较和交换的算法可能会使用地址的低位来指示指针成功修改的次数。因此,即使地址相同,下一个比较和交换也会失败,因为标记位不匹配。这有时被称为ABA',因为第二个A与第一个A略有不同。这种标记的状态引用也用在事务存储器中

如果“tag”字段包裹起来,则对ABA的保证不再存在。但是,已经观察到,在当前存在的CPU上,并且使用60位标签,只要程序寿命(即,不重新启动程序)限制为10年,就不可能进行环绕; 此外,有人认为,出于实际目的,通常有40-48位标签可以保证不会缠绕。由于现代CPU(特别是所有现代x64 CPU)倾向于支持128位CAS操作,因此在某些使用情况下,它允许在使用标记时提供针对ABA的坚定保证。[1]

中间节点

一种正确但昂贵的方法是使用非数据元素的中间节点,从而在插入和移除元素时确保不变量[Valois]延期回收编辑]

另一种方法是推迟回收已删除的数据元素。推迟回收的一种方法是在具有自动垃圾收集器的环境中运行算法; 然而,这里的一个问题是,如果GC不是无锁的,那么整个系统不是无锁的,即使数据结构本身也是如此。

推迟回收的另一种方法是使用一个或多个危险指针,它们是指向无法在列表中出现的位置的指针。每个危险指针代表正在进行的变化的中间状态; 指针的存在确保了进一步的同步[Doug Lea]。危险指针是无锁的,但最多只能追踪两个元素才能使用。

延迟回收的另一种方法是使用读取副本更新(RCU),其涉及将更新封装在RCU读取侧关键部分中,然后在释放任何移除的数据元素之前等待RCU宽限期。以这种方式使用RCU可确保在所有当前正在执行的操作完成之前,所有已删除的数据元素都不会重新出现。RCU是无锁的,但不适合所有工作负载。

一些体系结构提供“更大”的原子操作,例如,双链表中的前向和后向链接都可以原子方式更新; 虽然此功能依赖于体系结构,但它特别适用于x86 / x64体系结构(x86允许使用64位CAS,所有现代x64 CPU都允许使用128位CAS)。

一些体系结构提供了加载链接的存储条件指令,其中仅当没有指示位置的其他存储时才执行存储。这有效地将“存储包含值”的概念与“存储已被更改”区分开来。示例包括DEC Alpha,MIPS,PowerPC和ARM(v6及更高版本)。

<think>嗯,用户这次聚焦在CAS的ABA问题上,看来ta对乐观锁的实现细节很感兴趣。之前我已经详细解释过CAS的原理和ABA问题的成因,但用户似乎想更深入理解这个经典陷阱。 用户可能是后端开发工程师,正在设计高并发系统。ta提到站内引用[1][3][4]都涉及ABA问题,说明需要实际解决方案而非理论阐述。特别是引用[4]中AtomicStampedReference的代码示例很有价值,应该重点展开。 需要强调ABA问题在特定场景下的危险性:比如链表操作中,如果节点A被移除后又重新插入,CAS检查指针地址相同就会误判。这会导致链表结构损坏,比单纯数值错误更致命。 硬件层面也值得补充:现代CPU的LL/SC指令(Load-Link/Store-Conditional)能天然避免ABA问题,但x86架构的CMPXCHG指令仍有此缺陷。Java的解决方案本质上是在软件层模拟LL/SC机制。 用户可能还想知道:为什么AtomicMarkableReference用布尔标记而不用版本号?因为有些场景只需要区分“是否被修改过”,用整数版本号会造成空间浪费。这个细微差别可以放在相关问题里提示。 </think>乐观锁核心 CAS 中的 ABA 问题及其解决方案: --- ### 一、ABA 问题本质 **根本原因**:CAS 仅判断**值相等性**,无法感知值变化的历史过程。 **典型场景**:线程 $T_1$ 读取值 $A$ 后,$T_2$ 将值 $A \to B \to A$,$T_1$ 的 CAS 操作仍成功(误判未修改)[^1][^3]。 **风险示例**: ```mermaid sequenceDiagram T1->>内存: 读取 V=A T2->>内存: CAS(A→B) 成功 T2->>内存: CAS(B→A) 成功 T1->>内存: CAS(A→X) 成功(逻辑错误) ``` --- ### 二、解决方案:引入状态标记 #### 方案 1:版本号机制(**AtomicStampedReference**) 为值附加递增版本号,CAS 同时校验**值 + 版本号**: $$ \text{CAS}((V, \text{stamp}), (E, e_{\text{stamp}}), (N, n_{\text{stamp}})) $$ **代码实现**: ```java AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(100, 0); // 模拟ABA攻击 int[] stampHolder = new int[1]; int value = atomicRef.get(stampHolder); // value=100, stamp=0 atomicRef.compareAndSet(100, 50, stampHolder[0], stampHolder[0]+1); // 成功:100→50 (ver1) atomicRef.compareAndSet(50, 100, stampHolder[0], stampHolder[0]+1); // 成功:50→100 (ver2) // 线程尝试修改原始值 boolean success = atomicRef.compareAndSet(100, 200, 0, 1); // 失败!版本号已从0→2 System.out.println(success); // false ``` > 💡 **关键**:版本号从 `0→2`,而线程预期版本号为 `0`,CAS 失败[^4]。 #### 方案 2:状态标记机制(**AtomicMarkableReference**) 使用布尔值标记值状态(适用于无需精确版本的场景): ```java AtomicMarkableReference<Integer> ref = new AtomicMarkableReference<>(100, false); ref.compareAndSet(100, 101, false, true); // 修改时标记状态 ``` --- ### 三、ABA 问题的危害场景 | **场景** | **风险后果** | **防御方案** | |--------------------|-----------------------------|--------------------------| | 链表删除节点 | 误删已被重新加入的节点 | 节点指针 + 版本号 | | 无锁栈/队列 | 数据结构破坏 | 指针地址 + 计数器标记 | | 资源状态管理 | 重复执行已回滚的操作 | 状态机 + 时间戳 | --- ### 四、深度优化策略 1. **硬件辅助方案** ARM 架构的 `LDREX/STREX` 指令对通过**独占内存访问**天然规避 ABA 问题[^2]。 2. **混合锁机制** 高竞争环境下切换为轻量锁(如 Java 的 `synchronized` 优化): ```java if (casAttempts++ > SPIN_THRESHOLD) { synchronized(lock) { /* 退化到锁机制 */ } } ``` 3. **时间窗限制** 为操作添加时效性(如 Redis 的 `EX` 参数): ```java long deadline = System.currentTimeMillis() + TIMEOUT_MS; while (currentTime < deadline) { if (casOp()) return true; } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值