Skip to content

ABA问题从状态历史到版本戳完整原理

ABA表示共享状态从A变为B,又回到A。只比较当前值的CAS会认为“仍是A”,但若算法关心中间是否变化、对象是否被移除重插、业务状态是否经历过流转,这次成功可能建立在过期前提上。

一、ABA全过程

mermaid
flowchart TD
    A["T1读取值A和旧结构"] --> B["T1暂停"]
    B --> C["T2把A改为B"]
    C --> D["T2完成其他状态变化"]
    D --> E["T2把值改回A"]
    E --> F["T1执行CAS A→C"]
    F --> G["当前值是A所以CAS成功"]
    G --> H["但T1依据的历史/结构已过期"]

CAS没有“被骗”或实现错误,它只承诺比较当前值。若业务要求检测历史变化,调用方必须把版本纳入比较状态。

二、什么时候ABA无害

简单计数从5到6再回5,线程只关心“当前是否为5”,中间历史无业务含义,ABA可能无害。不能看到任何值恢复就机械使用版本戳。

有害条件通常是:

  • 读取值时还读取了与该版本绑定的其他结构。
  • 节点被删除、复用或重新插入。
  • 状态流转本身有审计/副作用意义。
  • 更新前提要求“自读取后从未改变”。
  • 外部资源句柄虽相同,生命周期已更换。

三、无锁栈为什么是经典场景

T1读到栈顶A和 A.next=B,准备把Top从A改为B。T2弹出A、弹出B,又把A压回;Top再次是A,但A的next和栈结构已变化。T1的CAS只比较Top引用可能成功,并把Top写成自己保存的旧B,破坏当前结构。

Java GC降低“节点内存释放后原地址分给另一对象”的风险,但同一个节点对象被移除后重新使用,或业务状态恢复原引用,逻辑ABA仍存在。GC不等于ABA消失。

四、版本戳怎样解决

把比较状态从:

text
value

扩展为:

text
(value, version)

每次成功修改都递增version:

text
(A,1) → (B,2) → (A,3)

T1期望 (A,1),当前虽然值为A,但版本为3,因此CAS失败。

版本必须与值作为同一个原子状态比较更新。把值和version放在两个独立Atomic变量中,仍可能读到不一致组合。

五、AtomicStampedReference

AtomicStampedReference<V> 同时保存引用和int stamp,CAS同时比较期望引用与期望stamp。

java
import java.util.concurrent.atomic.AtomicStampedReference;

public class AbaStampedDemo {
    public static void main(String[] args) {
        AtomicStampedReference<String> ref =
            new AtomicStampedReference<String>("A", 1);

        int[] stampHolder = new int[1];
        String oldReference = ref.get(stampHolder);
        int oldStamp = stampHolder[0];

        ref.compareAndSet("A", "B", 1, 2);
        ref.compareAndSet("B", "A", 2, 3);

        boolean success = ref.compareAndSet(
            oldReference, "C", oldStamp, oldStamp + 1
        );
        System.out.println("success=" + success);
        System.out.println("value=" + ref.getReference());
        System.out.println("stamp=" + ref.getStamp());
    }
}

JDK7/8均可运行,输出:

text
success=false
value=A
stamp=3

六、为什么读取引用和stamp要一起取

错误:

java
String value = ref.getReference();
int stamp = ref.getStamp();

两次调用之间可能发生更新,得到旧引用和新stamp的混合。应使用:

java
int[] holder = new int[1];
String value = ref.get(holder);
int stamp = holder[0];

API提供同一时刻关联快照。之后仍可能被修改,但CAS会通过期望stamp检测。

七、AtomicMarkableReference区别

AtomicMarkableReference<V> 保存引用和boolean标记,适合只关心“是否被逻辑删除/是否发生某类状态”的算法。

工具附加状态适合
AtomicStampedReferenceint版本需要区分多次变化
AtomicMarkableReferenceboolean标记只需要删除/有效等二态标志

boolean无法区分 false→true→false发生过几次,因此不能通用替代版本号。

八、版本号也有边界

  • int stamp理论上会溢出并回绕;极高频、长生命周期协议要评估位宽和回绕窗口。
  • 每次状态变化必须更新版本,漏更新就失去检测能力。
  • 版本戳只检测变化,不自动验证业务合法性。
  • 更新函数仍需无副作用或幂等。
  • 多个外部系统的一致性不能靠JVM内stamp解决。

可以使用更宽版本、不可复用唯一ID、不可变节点、锁或专门内存回收算法,取决于风险模型。

九、业务状态机中的ABA

订单状态:

text
待处理 → 处理中 → 待处理

当前文字又是“待处理”,但可能已经产生一次任务、审计或消息。旧操作若只按状态字符串更新,会误认为从未变化。

数据库常用:

sql
UPDATE task
SET status = ?, version = version + 1
WHERE id = ? AND version = ?;

受影响行数为0表示版本已变化,调用方重新读取和决策。它与AtomicStampedReference思想相似,但数据库还要处理事务、持久化和跨进程并发。

十、用不可变对象携带版本

java
final class State {
    final String status;
    final long version;

    State(String status, long version) {
        this.status = status;
        this.version = version;
    }
}

使用 AtomicReference<State> 整体替换,CAS比较读取到的旧State引用。每次创建新对象,即使status回到A,引用和version也不同。这适合多个字段必须一起变化的内存状态。

十一、ABA与业务幂等不同

  • ABA:旧前提在值恢复后被误认为仍成立。
  • 幂等:同一业务请求重复执行不会产生重复副作用。

版本戳阻止过期状态提交,但网络超时后客户端重试仍可能重复创建订单,需要业务幂等键和唯一约束。二者不能互相替代。

十二、怎样选择方案

mermaid
flowchart TD
    A["值可能恢复原状"] --> B{"算法关心中间历史吗"}
    B -- "否" --> C["普通CAS可能足够"]
    B -- "是" --> D{"只关心二态标记吗"}
    D -- "是" --> E["AtomicMarkableReference"]
    D -- "否" --> F{"单进程内存状态吗"}
    F -- "是" --> G["Stamped或不可变State整体CAS"]
    F -- "否" --> H["数据库version/事务/幂等协议"]

复杂无锁结构若正确性难以证明,使用锁通常比一个不完整版本戳方案更安全。

十三、常见误区

误区正确理解
只要值变回去就是Bug只有算法依赖历史时有害
Java有GC所以无ABAGC减少部分内存复用风险,逻辑ABA仍在
两个Atomic分别存值和版本不能保证读取/更新同一组合
stamp永不重复有溢出和漏更新边界
版本号解决重复扣款仍需业务幂等和事务
所有ABA都应写无锁算法锁可能更简单可靠

十四、面试标准回答

ABA是什么

线程读取A后暂停,其他线程把A改成B再改回A,原线程CAS只比较当前值仍可能成功。如果算法依赖“期间未改变”或旧结构,提交就基于过期前提。CAS实现没有错误,是比较状态不足。

怎样解决ABA

把版本与值作为同一原子状态比较,每次修改递增版本,例如AtomicStampedReference;只关心逻辑删除可用AtomicMarkableReference;多字段可用不可变State整体CAS;数据库跨进程并发使用version条件更新。复杂结构也可选择锁。

AtomicStampedReference能解决所有并发问题吗

不能。它检测引用和stamp变化,不保证业务状态合法、多个外部系统事务或重复请求幂等;stamp还存在回绕和漏更新边界。仍需按场景组合锁、事务、唯一约束和幂等。

十五、关联知识点

十六、学习验收

  • [ ] 能画出A→B→A和过期前提。
  • [ ] 能判断一个计数ABA是否有害。
  • [ ] 能解释无锁栈ABA。
  • [ ] 能运行AtomicStampedReference Demo。
  • [ ] 能说明为何引用和stamp要一起读取。
  • [ ] 能区分Stamped和Markable。
  • [ ] 能解释版本回绕和漏更新边界。
  • [ ] 能区分ABA、乐观锁和业务幂等。

本章小结

ABA的本质不是值长得一样,而是当前值不足以代表算法依赖的状态历史。若历史重要,就把版本、标记或不可变状态身份纳入同一次原子比较;若跨进程或有业务副作用,还要使用数据库版本、事务和幂等。正确方案从业务不变量出发,不是看到CAS就机械加stamp。