1. 概述
在之前的文章中我们提到过,AtomicStampedReference 可以有效避免并发中的 ABA 问题。
本文将深入探讨如何正确使用 AtomicStampedReference
,帮助你在高并发场景下写出更健壮的无锁代码。
2. 为什么需要 AtomicStampedReference?
✅ AtomicStampedReference
的核心优势在于:它提供了一个原子性读写操作的对象引用 + 时间戳(或版本号)的组合。
你可以把“stamp”理解为一个版本号或者逻辑时间戳。它的存在,就是为了检测一种特殊场景:
一个共享变量被其他线程从 A 改成 B,又改回 A —— 这就是经典的 ABA 问题。
⚠️ 普通的 AtomicReference
在 CAS(Compare-And-Swap)时只比较引用是否相等,无法察觉这种“先变后恢复”的中间变更。而 AtomicStampedReference
通过附加的 stamp,能精准识别这类变化。
举个简单粗暴的例子:你看到钱包里的钱还是 100 块,但其实已经被别人偷走又放回来了。AtomicReference
会认为“没变”,而 AtomicStampedReference
能发现“有人动过”。
3. 银行账户示例
我们以一个银行账户为例,包含两个关键数据:
- 账户余额(balance)
- 最后修改时间戳(stamp)
每次余额变动,stamp 都应递增,这样我们就能通过 stamp 判断账户是否被修改过。
3.1 读取值与 Stamp
初始化账户,余额为 100,初始 stamp 为 0:
AtomicStampedReference<Integer> account = new AtomicStampedReference<>(100, 0);
获取当前余额和 stamp:
Integer balance = account.getReference(); // 获取引用值
int stamp = account.getStamp(); // 获取当前 stamp
这两个方法是独立调用的。但在并发环境下,如果你先读 balance 再读 stamp,中间可能已被其他线程修改 —— 所以不能保证这两个值是同一时刻的快照。
3.2 原子更新值与 Stamp
要安全地更新账户,必须使用 compareAndSet
方法,同时比较当前值和 stamp 是否匹配:
if (!account.compareAndSet(balance, balance + 100, stamp, stamp + 1)) {
// 更新失败,说明期间有其他线程修改了账户
// 需要重试(retry)
}
❌ 踩坑点:很多人会分别读取 getReference()
和 getStamp()
,然后传入 compareAndSet
。这在高并发下极容易出错,因为两次读取之间状态可能已变更。
✅ 正确做法:使用 get(int[] stampHolder)
方法原子性地获取当前值和 stamp:
public class StampedAccount {
private final AtomicStampedReference<Integer> account;
private final AtomicInteger stamp = new AtomicInteger(0);
public StampedAccount() {
this.account = new AtomicStampedReference<>(0, 0);
}
public boolean withdrawal(int funds) {
int[] stamps = new int[1];
int current = this.account.get(stamps); // 原子性获取值和 stamp
int newStamp = this.stamp.incrementAndGet();
return this.account.compareAndSet(current, current - funds, stamps[0], newStamp);
}
public boolean deposit(int funds) {
int[] stamps = new int[1];
int current = this.account.get(stamps);
int newStamp = this.stamp.incrementAndGet();
return this.account.compareAndSet(current, current + funds, stamps[0], newStamp);
}
public int getBalance() {
return account.getReference();
}
public int getStamp() {
return account.getStamp();
}
}
📌 关键点解释:
account.get(stamps)
:将当前 stamp 写入stamps[0]
,同时返回当前 balancestamp.incrementAndGet()
:使用独立的AtomicInteger
生成递增版本号(也可以直接用stamps[0] + 1
,但建议分离管理)compareAndSet
:只有当引用和 stamp 都匹配时才更新,否则失败
3.3 ABA 场景模拟与检测
来看一个典型的 ABA 干扰场景:
- 初始 balance = 100,stamp = 0
- Thread-1 调用
deposit(100)
,读取到 balance=100, stamp=0,生成 newStamp=1,但尚未执行 CAS - Thread-2 执行:
deposit(100)
→ balance=200, stamp=1withdraw(100)
→ balance=100, stamp=2
- Thread-1 恢复,执行:
account.compareAndSet(100, 200, 0, 1) // ❌ 失败!
虽然 balance 还是 100,但 stamp 已经是 2,不再是 0。因此 CAS 失败,Thread-1 会进入重试流程。
✅ 结果:ABA 操作被成功拦截,保证了数据一致性。
3.4 单元测试验证
由于 ABA 依赖特定线程调度,难以稳定复现。但我们至少可以验证基本功能和并发安全性:
public class ThreadStampedAccountUnitTest {
@Test
public void givenMultiThread_whenStampedAccount_thenSetBalance() throws InterruptedException {
StampedAccount account = new StampedAccount();
Thread t1 = new Thread(() -> {
while (!account.deposit(100)) {
Thread.yield(); // CAS 失败则重试
}
});
Thread t2 = new Thread(() -> {
while (!account.withdrawal(100)) {
Thread.yield();
}
});
t1.start();
t2.start();
t1.join(10_000);
t2.join(10_000);
assertFalse(t1.isAlive());
assertFalse(t2.isAlive());
assertEquals(0, account.getBalance());
assertTrue(account.getStamp() > 0); // 至少有一次修改
}
}
📌 测试说明:
- 两个线程并发存取 100 元,最终余额应为 0
- 使用
while + yield
实现简单重试机制 - 断言 stamp > 0,确保版本号正常递增
3.5 如何选择下一个 Stamp?
关于 stamp 的生成策略,有几点建议:
✅ 推荐做法:
- 单调递增:最常见也最安全,可用
AtomicInteger
管理 - 逻辑版本号:每次修改递增 1,语义清晰
⚠️ 不推荐:
- 使用随机数:虽然可行,但无法保证唯一性和顺序性,可能增加冲突概率
- 时间戳(ms 级):精度不够,高并发下容易重复
📌 注意:AtomicStampedReference
本身不强制 stamp 递增,是否遵守由开发者保证。一旦 stamp 回退或重复,ABA 检测机制就失效了。
4. 总结
AtomicStampedReference
是解决 ABA 问题的利器,适用于以下场景:
- 高并发无锁数据结构(如链表、栈)
- 版本控制敏感的共享状态
- 需要精确检测“是否被修改过”的业务逻辑
与 AtomicReference
相比,它多了 stamp 的开销,但换来的是更强的一致性保障。
📌 使用要点:
要点 | 建议 |
---|---|
✅ 获取值和 stamp | 使用 get(int[]) 原子读取 |
✅ 更新操作 | compareAndSet 必须同时验证引用和 stamp |
✅ Stamp 管理 | 保持单调递增,建议用独立 AtomicInteger |
❌ 避免 | 分别调用 getReference() 和 getStamp() |
示例代码已托管至 GitHub:https://github.com/baeldung/core-java-modules/tree/master/core-java-concurrency-advanced-3