Java AtomicStampedReference解决ABA问题的版本号机制
在多线程环境中使用 CAS(Compare-And-Swap)操作时,会遇到一个被称为“ABA 问题”的典型陷阱。简单来说,就是一个共享变量的值从 A 变成了 B,又从 B 变回了 A。其他线程若只检查值,会误以为它从未被修改过,从而引发不可预期的错误。AtomicStampedReference 类通过引入“版本号”机制,完美解决了这一问题。
理解 ABA 问题的本质
想象这样一个场景:你桌上有一杯水(值 A),你离开了一会儿。期间,同事把水喝了(值变为空/B),然后又重新倒满了一杯新水(值又变回 A)。当你回来时,看到杯子里的水还是满的,以为这杯水一直没动过,拿起来就喝。实际上,这杯水已经被替换过了。
在编程中,这种“值未变但状态已变”的情况会导致严重的数据一致性问题,例如在链表操作中导致节点丢失。
解决方案核心:版本号机制
AtomicStampedReference 的核心思想是在维护对象引用的同时,维护一个整数类型的“版本号”(通常称为 Stamp)。
- 传统 CAS:只检查内存值 V 是否等于预期值 A。
- 带版本号的 CAS:检查内存值 V 是否等于预期值 A,并且检查当前版本号是否等于预期版本号。
只要对象被修改过,哪怕值变回了原样,版本号也必须递增。这样,CAS 操作就能通过对比版本号识别出值已经被改动过。
实操步骤与代码演示
下面通过对比 AtomicInteger(存在 ABA 问题)和 AtomicStampedReference(解决 ABA 问题),演示如何正确使用版本号机制。
1. 模拟 ABA 问题环境
我们需要两个线程:一个线程负责制造 ABA 变化,另一个线程负责尝试更新。
2. 编写对比代码
创建一个名为 ABADemo 的类,并在其中定义两个原子变量:一个普通的 AtomicInteger 和一个带版本号的 AtomicStampedReference。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
// 普通的原子类,存在 ABA 问题
private static AtomicInteger atomicInt = new AtomicInteger(100);
// 带版本号的原子引用,初始值为 100,初始版本号为 0
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<>(100, 0);
public static void main(String[] args) throws InterruptedException {
// --- 测试 AtomicInteger 的 ABA 问题 ---
System.out.println("--- 测试 AtomicInteger ---");
// **创建**线程 t1,模拟 ABA 过程:100 -> 101 -> 100
Thread t1 = new Thread(() -> {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
});
// **创建**线程 t2,尝试将 100 更新为 101
Thread t2 = new Thread(() -> {
try {
// **暂停** 1 秒,确保 t1 执行完 ABA 操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// **执行** CAS 操作
boolean isSuccess = atomicInt.compareAndSet(100, 101);
System.out.println("AtomicInteger CAS 结果: " + isSuccess); // 预期输出 true,但这其实是错误的成功
});
t1.start();
t2.start();
t1.join();
t2.join();
// --- 测试 AtomicStampedReference 解决 ABA ---
System.out.println("\n--- 测试 AtomicStampedReference ---");
// **创建**线程 refT1,模拟带版本号的 ABA 过程
Thread refT1 = new Thread(() -> {
try {
// **暂停** 1 秒,让主线程先拿到初始版本号
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// **获取**当前版本号
int stamp = atomicStampedRef.getStamp();
System.out.println("refT1 操作前版本号: " + stamp);
// **执行**更新:100 -> 101,版本号 +1
atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
// **执行**更新:101 -> 100,版本号再 +1
stamp = atomicStampedRef.getStamp(); // 获取新的版本号
atomicStampedRef.compareAndSet(101, 100, stamp, stamp + 1);
System.out.println("refT1 操作后版本号: " + atomicStampedRef.getStamp());
});
// **创建**线程 refT2,尝试利用旧版本号进行更新
Thread refT2 = new Thread(() -> {
// **获取**初始值和初始版本号(此时版本号还未被 refT1 修改)
int oldStamp = atomicStampedRef.getStamp();
System.out.println("refT2 拿到的初始版本号: " + oldStamp);
try {
// **暂停** 2 秒,确保 refT1 已经完成了 100->101->100 的过程
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// **尝试**更新:期望值 100,新值 101,期望版本号 oldStamp,新版本号 oldStamp + 1
boolean isSuccess = atomicStampedRef.compareAndSet(100, 101, oldStamp, oldStamp + 1);
System.out.println("AtomicStampedReference CAS 结果: " + isSuccess); // 预期输出 false
System.out.println("当前实际版本号: " + atomicStampedRef.getStamp());
});
refT1.start();
refT2.start();
}
}
3. 运行结果分析
执行上述代码,观察控制台输出。
对于 AtomicInteger,CAS 操作返回 true。这表明尽管值中间被修改过,但 CAS 依然认为没有变化,这也就是 ABA 问题的隐患所在。
对于 AtomicStampedReference,CAS 操作返回 false。
- 线程
refT2最初获取到版本号为0。 - 线程
refT1将值从 100 改为 101(版本号变为 1),又改回 100(版本号变为 2)。 - 线程
refT2醒来后尝试更新,它提供的期望版本号是0,但内存中实际的版本号已经是2。 - 因为版本号不匹配,更新被拒绝,从而保证了数据的安全性。
常用方法解析
在使用 AtomicStampedReference 时,主要涉及以下方法:
get():返回当前引用值及其对应的版本号。通常需要传入一个int数组来接收版本号。getReference():仅返回当前的引用对象。getStamp():仅返回当前的版本号(整数)。compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp):核心方法。如果当前引用等于expectedReference且当前版本号等于expectedStamp,则以原子方式将引用更新为newReference,将版本号更新为newStamp。attemptStamp(V expectedReference, int newStamp):如果当前引用持有预期的引用值,则以原子方式将版本号设置为newStamp。
通过在每一次修改数据时强制更新版本号,AtomicStampedReference 能够准确识别出“值虽然回到了原点,但中间发生过变化”的情况,是解决并发编程中 ABA 问题的标准方案。

暂无评论,快来抢沙发吧!