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],同时返回当前 balance
  • stamp.incrementAndGet():使用独立的 AtomicInteger 生成递增版本号(也可以直接用 stamps[0] + 1,但建议分离管理)
  • compareAndSet:只有当引用和 stamp 都匹配时才更新,否则失败

3.3 ABA 场景模拟与检测

来看一个典型的 ABA 干扰场景:

  1. 初始 balance = 100,stamp = 0
  2. Thread-1 调用 deposit(100),读取到 balance=100, stamp=0,生成 newStamp=1,但尚未执行 CAS
  3. Thread-2 执行:
    • deposit(100) → balance=200, stamp=1
    • withdraw(100) → balance=100, stamp=2
  4. 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


原始标题:Guide to AtomicStampedReference in Java