CAS中ABA问题及解决

一、ABA 场景描述

基于上节中提到的 CAS 原理,CAS 算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。设想一个场景:有线程 A、B 两个线程想对同一变量 Num = 1 进行修改,此时有:

第一步:线程 A 读取 Num 值为 1 。

第二步:线程 B 读取 Num 值为 1 。

第三步:线程 B 修改 Num 值为 2 。

第四步:线程 B 修改 Num 值为 1 。

第五步:线程 A 修改 Num 值为 2 。

以上场景可以看出,尽管线程 A 对 Num 的 CAS 操作进行成功了,但这不代表这个过程中没有问题。在没有垃圾回收机制的内存模型中(如C++),内存可随意释放:有线程 A、B 两个线程,

第一步:线程 A 从内存位置 V 中取出 M ,M 指向内存位置 W。

第二步:线程 B 从位置 V 中取出 M。

第三步:线程 B 进行了一些操作,释放了 M 指向的内存。

第四步:线程 B 重新申请内存,并恰好申请了内存位置 W,将位置 W 存入 N 的内容。

第五步:线程 B 将内存位置 W 写入位置 V。

第六步:线程 A 进行 CAS 操作,发现位置V中仍然是 M 指向的即内存位置 W,操作成功。

上述场景二要比场景一的问题更大,线程 A 与 线程 B 根本无法感知到 M 的内容已被修改,甚至如果线程 B 只释放内存,则 M 最后只会变成一个野指针。

二、AtomicStampedReference

Java 的垃圾回收机制已经帮我们解决了场景二,而场景一的解决加入版本号即可。

除了对象值,AtomicStampedReference 内部还维护了一个状态戳。状态戳可类比为时间戳,是一个整数值,每一次修改对象值的同时,也要修改状态戳,从而区分相同对象值的不同状态。当AtomicStampedReference 设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功。AtomicStampedReference 的几个 API 在 AtomicReference 的基础上新增了有关时间戳的信息:

//构造方法, 传入引用和戳
public AtomicStampedReference(V initialRef, int initialStamp)
//返回引用
public V getReference()
//返回版本戳
public int getStamp()
//如果当前引用 等于 预期值并且 当前版本戳等于预期版本戳, 将更新新的引用和新的版本戳到内存
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp)
//如果当前引用 等于 预期引用, 将更新新的版本戳到内存
public boolean attemptStamp(V expectedReference, int newStamp)
//设置当前引用的新引用和版本戳
public void set(V newReference, int newStamp) 

 

### ABA问题的本质 ABA问题CAS(Compare and Swap)操作中的一种典型问题,指的是一个值在被其他线程修改后,又被改回了原来的值。这种情况下,CAS操作会误认为值没有被改变,从而导致数据一致性问题。这种问题的核心在于内存中值的变化过程未被记录,无法反映其版本变化的历史[^3]。 ### ABA问题解决方案 为了解决ABA问题,通常需要引入额外的机制来记录值的变化过程。以下是两种常见的解决方案: 1. **版本号机制** 通过为值添加一个版本号,每次值发生变化时,版本号也会递增。在进行CAS操作时,不仅比较值是否相等,还需要比较版本号是否一致。这种方法确保了即使值被改回,只要版本号不同,CAS操作也会失败,从而避免误判。例如,Java中的`AtomicStampedReference`类就实现了这种机制,通过记录值的版本来解决ABA问题[^2]。 ```java // 示例代码:使用 AtomicStampedReference 解决 ABA 问题 import java.util.concurrent.atomic.AtomicStampedReference; public class ABAExample { public static void main(String[] args) { // 初始值和版本号 AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 0); Thread t1 = new Thread(() -> { int expectedValue = 100; int newValue = 50; int stamp = stampedReference.getStamp(); // 获取当前版本号 if (stampedReference.compareAndSet(expectedValue, newValue, stamp, stamp + 1)) { System.out.println("T1 更新值成功: " + newValue + ",版本号: " + stampedReference.getStamp()); } }); Thread t2 = new Thread(() -> { int expectedValue = 100; int newValue = 200; int stamp = stampedReference.getStamp(); // 获取当前版本号 if (stampedReference.compareAndSet(expectedValue, newValue, stamp, stamp + 1)) { System.out.println("T2 更新值成功: " + newValue + ",版本号: " + stampedReference.getStamp()); } }); t1.start(); t2.start(); } } ``` 2. **标志位机制** 另一种解决方案是使用标志位,通过记录值是否被修改过的状态来解决ABA问题Java中的`AtomicMarkableReference`类通过一个布尔值标志位来标记值是否被修改过。这种方法适用于只需要记录值是否发生变化的场景[^2]。 ```java // 示例代码:使用 AtomicMarkableReference 解决 ABA 问题 import java.util.concurrent.atomic.AtomicMarkableReference; public class ABAExample { public static void main(String[] args) { // 初始值和标志位 AtomicMarkableReference<Integer> markableReference = new AtomicMarkableReference<>(100, false); Thread t1 = new Thread(() -> { int expectedValue = 100; int newValue = 50; boolean[] mark = new boolean[1]; mark[0] = markableReference.isMarked(); // 获取当前标志位 if (markableReference.compareAndSet(expectedValue, newValue, mark[0], !mark[0])) { System.out.println("T1 更新值成功: " + newValue + ",标志位: " + markableReference.isMarked()); } }); Thread t2 = new Thread(() -> { int expectedValue = 100; int newValue = 200; boolean[] mark = new boolean[1]; mark[0] = markableReference.isMarked(); // 获取当前标志位 if (markableReference.compareAndSet(expectedValue, newValue, mark[0], !mark[0])) { System.out.println("T2 更新值成功: " + newValue + ",标志位: " + markableReference.isMarked()); } }); t1.start(); t2.start(); } } ``` ### 总结 通过引入版本号或标志位机制,可以有效解决CAS操作中的ABA问题。这些机制通过记录值的变化历史,确保了在并发环境下数据的一致性和正确性。尽管这些方法会带来额外的内存和计算开销,但在需要严格数据一致性的场景下,它们是不可或缺的解决方案[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值