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 问题的标准方案,通过版本号跟踪变化。
相关推荐
码事漫谈14 小时前
C++ 多线程开发:从零开始的完整指南
后端
9ilk14 小时前
【C++】--- 特殊类设计
开发语言·c++·后端
码事漫谈14 小时前
十字路口的抉择:B端与C端C++开发者的职业路径全解析
后端
提笔了无痕15 小时前
git基本了解、常用基本命令与使用
git·后端
java1234_小锋16 小时前
Spring IoC的实现机制是什么?
java·后端·spring
喵个咪16 小时前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:JWT 集成指南
后端·go
绝不收费—免费看不了了联系我16 小时前
Fastapi的单进程响应问题 和 解决方法
开发语言·后端·python·fastapi
喵个咪16 小时前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:OPA 集成指南:从原理到实践
后端·go
Victor35617 小时前
Netty(11) Netty的心跳机制是什么?为什么需要它?
后端
Victor35617 小时前
Netty(12)Netty支持哪些协议和传输方式?
后端