CAS如何解决ABA问题?

CAS(Compare-And-Swap) 操作在并发编程中非常高效,但它也会面临一个问题——ABA 问题。为了更好地理解如何解决这个问题,首先我们需要了解什么是 ABA 问题,然后看一下如何通过引入其他机制来解决这个问题。

✅ 一、什么是 ABA 问题?

在 CAS 操作中,假设线程 A 执行 CAS 操作时希望将一个共享变量的值从 V 更新为 V',但 CAS 操作有一个前提条件——它需要检查该变量的当前值是否和预期值相同。如果相同,则执行更新;如果不同,则 CAS 操作失败。

ABA 问题 就是:当一个线程执行 CAS 操作时,它预期的值是 V,但是在 CAS 操作执行之前,其他线程可能将该值从 V 改为 V' 再改回 V,这种情况下,CAS 操作会错误地认为值没有变化,从而执行了不应该执行的更新。

示例:
  • 初始值:V = 100

  • 线程 A 期望将 V 更新为 V' = 200,并且它会检查 V 是否为 100

  • 线程 B 将 V 改为 150,然后再将 V 改回 100

  • 线程 A 执行 CAS 操作时,看到 V 还是 100,就认为值没有改变,执行了更新操作,把 V 改为了 200

  • 然而,实际上,V 已经经历了变化,但线程 A 却没有意识到这一点。

✅ 二、CAS 如何解决 ABA 问题?

CAS 本身无法直接解决 ABA 问题,但通过一些附加的机制,我们可以解决这一问题。Java 提供了几种方法来避免 ABA 问题:


✅ 三、解决 ABA 问题的方法

1. 使用版本号(AtomicStampedReference

Java 中的 AtomicStampedReference 类通过引入 版本号 来解决 ABA 问题。每次更新 AtomicStampedReference 时,都会更新一个与之关联的版本号(stamp)。这样,CAS 操作不仅会检查值本身是否匹配,还会检查版本号是否匹配,从而避免了 ABA 问题。

实现原理:

  • 每次修改数据时,除了修改值,还会修改版本号(stamp)。

  • 在执行 CAS 操作时,除了比较值外,还会比较版本号,确保在检查值是否相等时,版本号没有变化。

示例:
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceExample {
    public static void main(String[] args) {
        // 初始值为 100,版本号为 0
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 0);

        // 获取当前值和版本号
        int[] stamp = new int[1];
        Integer value = atomicStampedReference.get(stamp);

        // 线程 A 执行 CAS 操作
        boolean success = atomicStampedReference.compareAndSet(value, 200, stamp[0], stamp[0] + 1);
        System.out.println("线程 A CAS 操作成功: " + success); // 输出: 线程 A CAS 操作成功: true

        // 线程 B 模拟将值修改为其他值后再恢复
        atomicStampedReference.set(150, stamp[0] + 1); // 设置为 150 并更新版本号
        atomicStampedReference.set(100, stamp[0] + 2); // 恢复为 100,并更新版本号

        // 线程 A 再次执行 CAS 操作
        value = atomicStampedReference.get(stamp);
        success = atomicStampedReference.compareAndSet(value, 200, stamp[0], stamp[0] + 1);
        System.out.println("线程 A CAS 操作成功: " + success); // 输出: 线程 A CAS 操作成功: false
    }
}

输出:

线程 A CAS 操作成功: true
线程 A CAS 操作成功: false

解释:

  • 线程 A 在第一次 CAS 操作时成功,因为值和版本号都匹配。

  • 线程 B 将值改为 150,再改回 100,并且更新了版本号。

  • 线程 A 再次执行 CAS 操作时,虽然值看起来是 100,但版本号已经不同,所以 CAS 操作失败,避免了 ABA 问题。

2. 使用标记位(AtomicMarkableReference

AtomicMarkableReference 是 Java 提供的另一种解决方案,类似于 AtomicStampedReference,不过它使用 布尔标记 来代替版本号。每次更新数据时,会改变一个布尔标记位,从而确保 CAS 操作不仅检查值,还检查标记位,避免了 ABA 问题。

示例:
import java.util.concurrent.atomic.AtomicMarkableReference;

public class AtomicMarkableReferenceExample {
    public static void main(String[] args) {
        // 初始值为 100,标记位为 false
        AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<>(100, false);

        // 获取当前值和标记
        boolean[] mark = new boolean[1];
        Integer value = atomicMarkableReference.get(mark);

        // 线程 A 执行 CAS 操作
        boolean success = atomicMarkableReference.compareAndSet(value, 200, mark[0], !mark[0]);
        System.out.println("线程 A CAS 操作成功: " + success); // 输出: 线程 A CAS 操作成功: true

        // 线程 B 模拟将值修改为其他值后再恢复
        atomicMarkableReference.set(150, !mark[0]); // 设置为 150 并更新标记
        atomicMarkableReference.set(100, mark[0]); // 恢复为 100,并更新标记

        // 线程 A 再次执行 CAS 操作
        value = atomicMarkableReference.get(mark);
        success = atomicMarkableReference.compareAndSet(value, 200, mark[0], !mark[0]);
        System.out.println("线程 A CAS 操作成功: " + success); // 输出: 线程 A CAS 操作成功: false
    }
}

输出:

线程 A CAS 操作成功: true
线程 A CAS 操作成功: false

解释:

  • 线程 A 在第一次 CAS 操作时成功。

  • 线程 B 修改了值并更新了标记位。

  • 线程 A 再次执行 CAS 操作时,标记位已经改变,所以 CAS 操作失败,避免了 ABA 问题。


✅ 四、总结

CAS 操作本身无法避免 ABA 问题,但通过引入 版本号AtomicStampedReference)或者 标记位AtomicMarkableReference),可以有效解决这一问题。这样,CAS 操作不仅会比较数据值,还会比较版本号或标记,从而确保在更新数据时,数据的变动历史可以被正确检测到,避免了因数据值恢复原状而导致的错误更新。

如果你有更多关于 CAS 或并发编程的问题,欢迎继续提问!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值