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 问题的标准方案,通过版本号跟踪变化。
相关推荐
smileNicky1 分钟前
SpringBoot系列之集成Pulsar教程
java·spring boot·后端
小翰子_35 分钟前
Spring Boot整合Sharding-JDBC实现日志表按月按周分表实战
java·spring boot·后端
踏浪无痕1 小时前
SQLInsight:从JDBC底层到API调用的零侵入SQL监控方案
数据库·后端·开源
superman超哥2 小时前
Rust HashSet与BTreeSet的实现细节:集合类型的底层逻辑
开发语言·后端·rust·编程语言·rust hashset·rust btreeset·集合类型
superman超哥3 小时前
Rust String与&str的内部实现差异:所有权与借用的典型案例
开发语言·后端·rust·rust string·string与str·内部实现·所有权与借用
愈努力俞幸运4 小时前
rust安装
开发语言·后端·rust
踏浪无痕4 小时前
JobFlow 负载感知调度:把任务分给最闲的机器
后端·架构·开源
UrbanJazzerati4 小时前
Python自动化统计工具实战:Python批量分析Salesforce DML操作与错误处理
后端·面试
我爱娃哈哈4 小时前
SpringBoot + Seata + Nacos:分布式事务落地实战,订单-库存一致性全解析
spring boot·分布式·后端
nil4 小时前
记录protoc生成代码将optional改成omitepty问题
后端·go·protobuf