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 或并发编程的问题,欢迎继续提问!