cas 存在的典型ABA 问题举例

更合适的例子:无锁栈(Treiber Stack)的 ABA 问题

ABA 问题在 无锁数据结构 (如栈、队列)中更常见,而余额修改场景确实不太典型。我们换一个更合适的例子:无锁栈的 ABA 问题


1. 问题场景:无锁栈(Treiber Stack)

实现一个线程安全的栈 ,使用 AtomicReference 管理栈顶:

java 复制代码
import java.util.concurrent.atomic.AtomicReference;

class ConcurrentStack<T> {
    private static class Node<T> {
        final T value;
        Node<T> next;

        Node(T value) {
            this.value = value;
        }
    }

    private final AtomicReference<Node<T>> top = new AtomicReference<>();

    public void push(T item) {
        Node<T> newNode = new Node<>(item);
        Node<T> oldTop;
        do {
            oldTop = top.get();
            newNode.next = oldTop;
        } while (!top.compareAndSet(oldTop, newNode)); // CAS 更新栈顶
    }

    public T pop() {
        Node<T> oldTop;
        Node<T> newTop;
        do {
            oldTop = top.get();
            if (oldTop == null) {
                return null; // 空栈
            }
            newTop = oldTop.next;
        } while (!top.compareAndSet(oldTop, newTop)); // CAS 更新栈顶
        return oldTop.value;
    }
}

2. ABA 问题如何发生?

假设栈初始状态:

css 复制代码
top -> A -> B -> C

线程 1 执行 pop()

  1. 读取 oldTop = A,准备 CAS 更新栈顶为 B
  2. 被挂起(还未执行 CAS)。

线程 2 执行两次 pop()

  1. 弹出 A,栈变为 B -> C
  2. 弹出 B,栈变为 C
  3. push(A),栈变回 A -> C

线程 1 恢复执行 : • 执行 top.compareAndSet(A, B),发现 top 仍然是 A(ABA 发生!)。 • CAS 错误地成功 ,导致栈顶被设为 B(但 B 已经被弹出过,可能已被其他线程修改)。

最终栈状态

css 复制代码
top -> B   // 但 B 的 next 可能已经失效(如指向一个已删除的节点)

问题

• 栈结构被破坏,可能导致后续操作出错(如 NullPointerException)。 • 即使值看起来没变(ABA),但中间状态已被篡改。


3. 解决方案:AtomicStampedReference

通过 版本号(Stamp) 跟踪栈顶变化:

java 复制代码
import java.util.concurrent.atomic.AtomicStampedReference;

class SafeConcurrentStack<T> {
    private static class Node<T> {
        final T value;
        Node<T> next;

        Node(T value) {
            this.value = value;
        }
    }

    private final AtomicStampedReference<Node<T>> top = 
        new AtomicStampedReference<>(null, 0);

    public void push(T item) {
        Node<T> newNode = new Node<>(item);
        int[] stampHolder = new int[1];
        Node<T> oldTop;
        do {
            oldTop = top.get(stampHolder); // 获取当前栈顶和版本号
            newNode.next = oldTop;
        } while (!top.compareAndSet(oldTop, newNode, stampHolder[0], stampHolder[0] + 1));
    }

    public T pop() {
        int[] stampHolder = new int[1];
        Node<T> oldTop;
        Node<T> newTop;
        do {
            oldTop = top.get(stampHolder);
            if (oldTop == null) {
                return null;
            }
            newTop = oldTop.next;
        } while (!top.compareAndSet(oldTop, newTop, stampHolder[0], stampHolder[0] + 1));
        return oldTop.value;
    }
}

关键改进 : • 每次修改栈顶时,版本号(Stamp)递增。 • CAS 不仅比较引用,还比较版本号,确保中间未被其他线程修改。


4. 为什么余额修改不需要担心 ABA?

在余额修改场景(如 AtomicInteger):

java 复制代码
AtomicInteger balance = new AtomicInteger(100);

// 线程1:读取 balance=100,准备扣款 50(预期余额 100)
int oldBalance = balance.get();
int newBalance = oldBalance - 50;

// 线程2:临时修改 balance 为 200,又改回 100
balance.set(200);
balance.set(100);

// 线程1:执行 CAS(100 → 50)
balance.compareAndSet(100, 50); // 仍然成功

为什么没问题?

• 余额从 100200100,虽然值相同,但业务逻辑上 没有副作用 : • 只要最终余额正确(100 - 50 = 50),中间变化不影响结果。 • ABA 问题仅在数据结构依赖中间状态时才有害(如栈的节点指针被篡改)。


5. 总结

场景 是否需要解决 ABA 问题? 推荐工具
无锁栈/队列 ✅ 需要 AtomicStampedReference
余额修改(单变量) ❌ 不需要 AtomicInteger
状态机/版本控制 ✅ 需要 AtomicStampedReference

关键结论

  1. ABA 问题在无锁数据结构中更严重(如栈、队列、链表),因为指针可能被篡改。
  2. 余额修改等单变量操作通常不受影响,因为只关心最终值。
  3. AtomicStampedReference 是解决 ABA 问题的标准方案,通过版本号跟踪变化。
相关推荐
小Ti客栈4 小时前
Spring Boot 集成 Springdoc-OpenAPI 与 Knife4j实现接口文档与可视化调试
java·spring boot·后端
Ai拆代码的曹操4 小时前
Spring 事务 REQUIRES_NEW 嵌套调用:连接池翻倍的秘密
java·后端·spring
IT_陈寒6 小时前
SpringBoot这个分页坑,我踩了三天才爬出来
前端·人工智能·后端
颜酱6 小时前
05 | 召回前置准备:根据业务数据库生成各数据库(读取配置阶段)
前端·人工智能·后端
Wang's Blog6 小时前
Go-Zero项目开发24: 基于Bitmap实现群聊消息已读未读
开发语言·后端·golang
Conan在掘金7 小时前
鸿蒙报错速查:struct 里嵌 namespace 声明就炸,根因 + 真解法
后端
东方小月7 小时前
从零开发一个 Coding Agent(三):EventStream 事件流通道设计与实现
前端·人工智能·后端
卫子miao7 小时前
如何评价当前大语言模型的记忆机制?
后端·架构
神奇小汤圆8 小时前
为什么 AQS 成为 Java 并发的基石?
后端
星栈9 小时前
MCP 从 stdio 迁到 SSE,踩了 5 个传输层坑
人工智能·后端·架构