CAS中的ABA问题

补档CAS中的ABA问题。

要特别注意,常见的ABA问题有两种,要求能分别举例解释。

CAS的使用可参考:

1 定义

1.1 基本的ABA问题

在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并替换(由CPU完成,该操作是原子的)。这个时间差中,会导致数据的变化。

假设如下事件序列:

  1. 线程 1 从内存位置V中取出A。
  2. 线程 2 从位置V中取出A。
  3. 线程 2 进行了一些操作,将B写入位置V。
  4. 线程 2 将A再次写入位置V。
  5. 线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。

尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失

1.2 与内存模型相关的ABA问题

在没有垃圾回收机制的内存模型中(如C++),程序员可随意释放内存。

假设如下事件序列:

  1. 线程 1 从内存位置V中取出A,A指向内存位置W。
  2. 线程 2 从位置V中取出A。
  3. 线程 2 进行了一些操作,释放了A指向的内存。
  4. 线程 2 重新申请内存,并恰好申请了内存位置W,将位置W存入C的内容。
  5. 线程 2 将内存位置W写入位置V。
  6. 线程 1 进行CAS操作,发现位置V中仍然是A指向的即内存位置W,操作成功

这里比问题 1.1 的后果更严重,实际内容已经被修改了,但_线程 1 无法感知到线程 2 的修改_。

更甚,如果线程 2 只释放了A指向的内存,而线程 1 在 CAS之前还要访问A中的内容,那么线程 1 将访问到一个野指针

2 举例

2.1 基本的ABA问题举例

如果位置V存储的是链表的头结点,那么发生ABA问题的链表中,原头结点是node1,线程 2 操作头结点变化了两次,很可能是先修改头结点为node2,再将node1(在C++中,也可是重新分配的节点node3,但恰好其指针等于已经释放掉的node1)插入表头成为新的头结点。

对于线程 1 ,头结点仍旧为 node1(或者说头结点的值,因为在C++中,虽然地址相同,但其内容可能变为了node3),CAS操作成功,但头结点之后的子链表的状态已不可预知。

脑补示意图。。

2.2 与内存模型相关的ABA问题举例

问题定义已阐述清楚。

3 解决

Java的垃圾回收机制已经帮我们解决了问题 1.2;至于问题 1.1,加入版本号即可解决。

3.1 AtomicStampedReference

除了对象值,AtomicStampedReference内部还维护了一个“状态戳”。状态戳可类比为时间戳,是一个整数值,每一次修改对象值的同时,也要修改状态戳,从而区分相同对象值的不同状态。当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功。

AtomicStampedReference的几个API在AtomicReference的基础上新增了有关时间戳的信息:

//比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳
public boolean compareAndSet(V expectedReference, V newReference, 
    int expectedStamp, int newStamp)
//获得当前对象引用
public V getReference()
//获得当前时间戳
public int getStamp()
//设置当前对象引用和时间戳
public void set(V newReference, int newStamp)
复制代码

3.2 AtomicMarkableReference

AtomicMarkableReference和AtomicStampedReference功能相似,但AtomicMarkableReference描述更加简单的是与否的关系。它的定义就是将状态戳简化为true|false。如下:

public final static AtomicMarkableReference<String> ATOMIC_MARKABLE_REFERENCE 
    = new AtomicMarkableReference<>("abc" , false); 
复制代码

操作时:

ATOMIC_MARKABLE_REFERENCE.compareAndSet("abc", "abc2", false, true);
复制代码

本文链接:CAS中的ABA问题
作者:猴子007
出处:monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。

### CAS 中的 ABA 问题及其解决方案 #### 什么是 CASCAS(Compare-And-Swap)是一种用于实现原子操作的技术,广泛应用于多线程环境下的并发编程中。它通过比较内存中的预期值是否一致来决定是否更新为新值[^3]。 #### ABA 问题的定义 ABA 问题是指在多线程环境下,一个共享变量的值从 A 变为 B,然后又从 B 变回 A,而 CAS 操作可能会错误地认为该值没有被修改过。这种误判可能导致逻辑错误或不正确的程序行为[^4]。 #### ABA 问题的示例代码 以下是一个简单的代码示例,展示了 ABA 问题的发生: ```java import java.util.concurrent.atomic.AtomicReference; public class ABADemo { static AtomicReference<String> ref = new AtomicReference<>("A"); public static void main(String[] args) throws InterruptedException { // 获取初始值 A String prev = ref.get(); // 启动线程 t1 将值从 A 改为 B new Thread(() -> { System.out.println("t1: change A->B " + ref.compareAndSet(ref.get(), "B")); }, "t1").start(); // 线程 t2 将值从 B 改回 A Thread.sleep(500); // 确保 t1 先执行 new Thread(() -> { System.out.println("t2: change B->A " + ref.compareAndSet(ref.get(), "A")); }, "t2").start(); // 主线程尝试将值从 A 改为 C Thread.sleep(1000); // 确保 t1 和 t2 执行完毕 System.out.println("main: change A->C " + ref.compareAndSet(prev, "C")); } } ``` 在上述代码中,主线程期望将值从 A 改为 C,但由于其他线程的操作导致值从 A 到 B 再回到 A,CAS 操作会错误地认为值未被修改,从而允许将值改为 C[^4]。 #### 解决 ABA 问题的方法 为了防止 ABA 问题的发生,可以引入版本号机制,确保每次修改时不仅检查值是否相同,还检查版本号是否匹配。 ##### 使用 `AtomicStampedReference` `AtomicStampedReference` 是 Java 提供的一种解决 ABA 问题的工具类。它通过引入一个额外的“戳记”(stamp)来记录值的变化次数,从而避免单纯的值比较带来的误判。 ###### 示例代码 以下是一个使用 `AtomicStampedReference` 解决 ABA 问题的示例: ```java import java.util.concurrent.atomic.AtomicStampedReference; public class AtomicStampedReferenceDemo { static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); public static void main(String[] args) throws InterruptedException { int[] stampHolder = {ref.getStamp()}; // 启动线程 t1 将值从 A 改为 B,并增加戳记 new Thread(() -> { boolean success = ref.compareAndSet("A", "B", ref.getStamp(), ref.getStamp() + 1); if (success) { System.out.println("t1: change A->B"); } }, "t1").start(); // 启动线程 t2 将值从 B 改回 A,并再次增加戳记 Thread.sleep(500); // 确保 t1 先执行 new Thread(() -> { boolean success = ref.compareAndSet("B", "A", ref.getStamp(), ref.getStamp() + 1); if (success) { System.out.println("t2: change B->A"); } }, "t2").start(); // 主线程尝试将值从 A 改为 C,但需要检查戳记是否匹配 Thread.sleep(1000); // 确保 t1 和 t2 执行完毕 boolean success = ref.compareAndSet("A", "C", stampHolder[0], stampHolder[0] + 1); if (success) { System.out.println("main: change A->C"); } else { System.out.println("main: failed to change A->C due to stamp mismatch"); } } } ``` 在上述代码中,主线程通过检查戳记是否匹配来判断值是否真的未被修改,从而避免了 ABA 问题的发生[^1]。 #### 总结 ABA 问题CAS 操作中常见的一个问题,可能导致程序逻辑错误。通过引入版本号机制,例如使用 `AtomicStampedReference`,可以有效解决这一问题。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值