什么是ABA问题

ABA问题是CAS操作中的一个经典并发陷阱:线程读取变量值为A,准备CAS更新时,虽然变量值仍然是A,但实际上变量可能已经被其他线程修改为B,然后又改回A,导致CAS无法察觉中间状态的变化。

问题示意

时刻T0:变量值 = A
线程1:读取A,准备改为C

时刻T1:线程2将A改为B
时刻T2:线程2将B改回A

时刻T3:线程1执行CAS(A, C) → 成功!
        但线程1不知道变量经历了 A→B→A 的变化

问题复现

示例1:AtomicInteger的ABA

public class ABADemo {
    static AtomicInteger atomicInt = new AtomicInteger(100);
    
    public static void main(String[] args) throws InterruptedException {
        // 线程1
        Thread t1 = new Thread(() -> {
            int value = atomicInt.get();
            System.out.println("线程1读取值:" + value);
            
            try {
                Thread.sleep(1000);  // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // CAS成功,但不知道中间被改过
            if (atomicInt.compareAndSet(value, 101)) {
                System.out.println("线程1 CAS成功,修改为101");
            }
        }, "线程1");
        
        // 线程2:制造ABA
        Thread t2 = new Thread(() -> {
            // A → B
            atomicInt.compareAndSet(100, 200);
            System.out.println("线程2改为200");
            
            // B → A
            atomicInt.compareAndSet(200, 100);
            System.out.println("线程2改回100");
        }, "线程2");
        
        t1.start();
        Thread.sleep(100);  // 确保t1先读取
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("最终值:" + atomicInt.get());
    }
}

输出结果

线程1读取值:100
线程2改为200
线程2改回100
线程1 CAS成功,修改为101
最终值:101

问题:线程1的CAS成功了,但它不知道值在中间经历了100→200→100的变化。

示例2:链表的ABA问题(更严重)

public class ABALinkedListDemo {
    
    static class Node {
        int value;
        Node next;
        
        Node(int value) {
            this.value = value;
        }
        
        @Override
        public String toString() {
            return "Node{" + value + "}";
        }
    }
    
    static AtomicReference<Node> head = new AtomicReference<>();
    
    public static void main(String[] args) throws InterruptedException {
        // 初始化链表:A → B → C
        Node nodeA = new Node(1);
        Node nodeB = new Node(2);
        Node nodeC = new Node(3);
        
        nodeA.next = nodeB;
        nodeB.next = nodeC;
        head.set(nodeA);
        
        System.out.println("初始链表:A → B → C");
        
        // 线程1:想要执行CAS(A, B),即删除A节点
        Thread t1 = new Thread(() -> {
            Node oldHead = head.get();  // 读取A
            System.out.println("线程1读取头节点:" + oldHead);
            
            try {
                Thread.sleep(1000);  // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            Node newHead = oldHead.next;  // B节点
            // CAS:期望头节点是A,替换为B
            if (head.compareAndSet(oldHead, newHead)) {
                System.out.println("线程1 CAS成功,头节点改为:" + newHead);
            }
        }, "线程1");
        
        // 线程2:删除A和B,再把A插回去
        Thread t2 = new Thread(() -> {
            Node oldHead = head.get();  // A
            Node nodeB = oldHead.next;  // B
            Node nodeC = nodeB.next;    // C
            
            // 删除A和B,头节点变为C
            head.compareAndSet(oldHead, nodeC);
            System.out.println("线程2删除A和B,头节点变为C");
            
            // 重新插入A(A的next指向C)
            oldHead.next = nodeC;
            head.compareAndSet(nodeC, oldHead);
            System.out.println("线程2重新插入A,链表变为:A → C");
        }, "线程2");
        
        t1.start();
        Thread.sleep(100);  // 确保t1先读取
        t2.start();
        
        t1.join();
        t2.join();
        
        // 输出最终链表
        System.out.print("最终链表:");
        Node current = head.get();
        while (current != null) {
            System.out.print(current.value);
            current = current.next;
            if (current != null) System.out.print(" → ");
        }
        System.out.println();
    }
}

输出结果

初始链表:A → B → C
线程1读取头节点:Node{1}
线程2删除A和B,头节点变为C
线程2重新插入A,链表变为:A → C
线程1 CAS成功,头节点改为:Node{2}
最终链表:2 → 3

严重问题

  • 线程1的CAS成功了(因为头节点确实是A)
  • 但线程1将头节点设为B(原来的B.next)
  • 问题是B节点的next已经被线程2修改过,指向C
  • 导致节点A丢失,链表结构错误

ABA问题的危害

1. 数据不一致

public class DataInconsistency {
    static AtomicReference<Account> accountRef = new AtomicReference<>();
    
    static class Account {
        int balance;
        Account(int balance) { this.balance = balance; }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account(100);
        accountRef.set(account);
        
        // 线程1:检查余额≥100,准备扣款
        Thread t1 = new Thread(() -> {
            Account oldAccount = accountRef.get();
            if (oldAccount.balance >= 100) {
                System.out.println("线程1检查余额充足:" + oldAccount.balance);
                
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                // CAS扣款
                Account newAccount = new Account(oldAccount.balance - 100);
                if (accountRef.compareAndSet(oldAccount, newAccount)) {
                    System.out.println("线程1扣款成功,余额:" + newAccount.balance);
                }
            }
        });
        
        // 线程2:制造ABA
        Thread t2 = new Thread(() -> {
            Account oldAccount = accountRef.get();
            
            // 扣款100
            Account temp = new Account(oldAccount.balance - 100);
            accountRef.compareAndSet(oldAccount, temp);
            System.out.println("线程2扣款100,余额:" + temp.balance);
            
            // 充值100
            Account newAccount = new Account(temp.balance + 100);
            accountRef.compareAndSet(temp, newAccount);
            System.out.println("线程2充值100,余额:" + newAccount.balance);
        });
        
        t1.start();
        Thread.sleep(100);
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("最终余额:" + accountRef.get().balance);
        // 问题:余额变成0了,但线程1在检查时余额是足够的
    }
}

2. 内存回收问题

// 场景:对象池
public class ObjectPoolABA {
    static class Resource {
        boolean inUse = false;
    }
    
    static AtomicReference<Resource> pool = new AtomicReference<>();
    
    // 线程1:获取资源
    Resource r1 = pool.get();  // 获取资源A
    // ... 使用资源A
    
    // 线程2:释放资源A,重新分配给线程3
    pool.compareAndSet(r1, null);     // 释放A
    Resource r2 = new Resource();     // 创建新资源
    pool.compareAndSet(null, r1);     // 重新放入A
    
    // 线程1:归还资源(CAS成功,但可能破坏对象池状态)
    pool.compareAndSet(r1, null);     // ABA问题
}

解决方案

方案1:AtomicStampedReference(版本号)

public class AtomicStampedReferenceSolution {
    // 带版本号的原子引用
    static AtomicStampedReference<Integer> stampedRef = 
        new AtomicStampedReference<>(100, 0);  // 初始值100,版本号0
    
    public static void main(String[] args) throws InterruptedException {
        // 线程1
        Thread t1 = new Thread(() -> {
            int[] stampHolder = new int[1];
            Integer value = stampedRef.get(stampHolder);
            int stamp = stampHolder[0];
            System.out.println("线程1读取:value=" + value + ", stamp=" + stamp);
            
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // CAS:期望值100,版本号0,新值101,新版本号1
            if (stampedRef.compareAndSet(value, 101, stamp, stamp + 1)) {
                System.out.println("线程1 CAS成功");
            } else {
                int currentStamp = stampedRef.getStamp();
                System.out.println("线程1 CAS失败(检测到ABA),当前版本号:" + currentStamp);
            }
        }, "线程1");
        
        // 线程2:制造ABA
        Thread t2 = new Thread(() -> {
            int[] stampHolder = new int[1];
            Integer value = stampedRef.get(stampHolder);
            int stamp = stampHolder[0];
            
            // A → B(版本号0 → 1)
            stampedRef.compareAndSet(value, 200, stamp, stamp + 1);
            System.out.println("线程2改为200,版本号:" + stampedRef.getStamp());
            
            // B → A(版本号1 → 2)
            value = stampedRef.getReference();
            stamp = stampedRef.getStamp();
            stampedRef.compareAndSet(value, 100, stamp, stamp + 1);
            System.out.println("线程2改回100,版本号:" + stampedRef.getStamp());
        }, "线程2");
        
        t1.start();
        Thread.sleep(100);
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("最终:value=" + stampedRef.getReference() + 
                           ", stamp=" + stampedRef.getStamp());
    }
}

输出结果

线程1读取:value=100, stamp=0
线程2改为200,版本号:1
线程2改回100,版本号:2
线程1 CAS失败(检测到ABA),当前版本号:2
最终:value=100, stamp=2

原理

  • 每次更新都递增版本号
  • CAS不仅比较值,还比较版本号
  • 即使值相同,版本号不同也会失败

方案2:AtomicMarkableReference(布尔标记)

public class AtomicMarkableReferenceSolution {
    
    static class Node {
        int value;
        Node(int value) { this.value = value; }
        
        @Override
        public String toString() {
            return "Node{" + value + "}";
        }
    }
    
    // 带标记位的引用(标记:是否被删除)
    static AtomicMarkableReference<Node> markableRef;
    
    public static void main(String[] args) throws InterruptedException {
        Node node = new Node(100);
        markableRef = new AtomicMarkableReference<>(node, false);
        
        // 线程1:尝试更新未删除的节点
        Thread t1 = new Thread(() -> {
            boolean[] markHolder = new boolean[1];
            Node oldNode = markableRef.get(markHolder);
            boolean marked = markHolder[0];
            
            System.out.println("线程1读取:" + oldNode + ", 已删除:" + marked);
            
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // CAS:期望未删除的node,更新为新节点
            Node newNode = new Node(200);
            if (markableRef.compareAndSet(oldNode, newNode, false, false)) {
                System.out.println("线程1 CAS成功");
            } else {
                boolean currentMark = markableRef.isMarked();
                System.out.println("线程1 CAS失败,当前标记:" + currentMark);
            }
        }, "线程1");
        
        // 线程2:标记为已删除
        Thread t2 = new Thread(() -> {
            boolean[] markHolder = new boolean[1];
            Node oldNode = markableRef.get(markHolder);
            
            // 标记为已删除
            markableRef.attemptMark(oldNode, true);
            System.out.println("线程2标记为已删除");
        }, "线程2");
        
        t1.start();
        Thread.sleep(100);
        t2.start();
        
        t1.join();
        t2.join();
        
        boolean marked = markableRef.isMarked();
        System.out.println("最终:" + markableRef.getReference() + ", 已删除:" + marked);
    }
}

输出结果

线程1读取:Node{100}, 已删除:false
线程2标记为已删除
线程1 CAS失败,当前标记:true
最终:Node{100}, 已删除:true

适用场景

  • 不需要精确版本号,只需要知道”是否被修改过”
  • 标记删除、逻辑删除等场景

方案3:不可变对象

public class ImmutableSolution {
    
    // 使用不可变对象
    static final class ImmutableAccount {
        final int balance;
        final long version;
        
        ImmutableAccount(int balance, long version) {
            this.balance = balance;
            this.version = version;
        }
        
        ImmutableAccount withBalance(int newBalance) {
            return new ImmutableAccount(newBalance, version + 1);
        }
    }
    
    static AtomicReference<ImmutableAccount> accountRef = 
        new AtomicReference<>(new ImmutableAccount(100, 0));
    
    /**
     * 扣款操作
     */
    public static boolean deduct(int amount) {
        ImmutableAccount oldAccount, newAccount;
        do {
            oldAccount = accountRef.get();
            if (oldAccount.balance < amount) {
                return false;  // 余额不足
            }
            newAccount = oldAccount.withBalance(oldAccount.balance - amount);
        } while (!accountRef.compareAndSet(oldAccount, newAccount));
        
        return true;
    }
}

优点

  • 对象不可变,每次修改都创建新对象
  • 版本号自动递增
  • 天然解决ABA问题

方案4:业务层面避免

public class BusinessSolution {
    
    /**
     * 通过业务逻辑规避ABA
     */
    static class Order {
        String orderId;
        OrderStatus status;
        long timestamp;  // 时间戳
        
        enum OrderStatus {
            CREATED, PAID, CANCELLED
        }
    }
    
    static AtomicReference<Order> orderRef = new AtomicReference<>();
    
    /**
     * 取消订单:只能从CREATED状态取消
     */
    public static boolean cancelOrder() {
        Order oldOrder, newOrder;
        do {
            oldOrder = orderRef.get();
            
            // 业务规则:只能取消CREATED状态的订单
            if (oldOrder.status != Order.OrderStatus.CREATED) {
                return false;  // 状态不对,无法取消
            }
            
            newOrder = new Order();
            newOrder.orderId = oldOrder.orderId;
            newOrder.status = Order.OrderStatus.CANCELLED;
            newOrder.timestamp = System.currentTimeMillis();
            
        } while (!orderRef.compareAndSet(oldOrder, newOrder));
        
        return true;
    }
    
    // 即使发生ABA(CREATED → PAID → CREATED),
    // 第二次的CREATED状态在业务上也不应该存在,
    // 通过状态机设计避免ABA的危害
}

ABA问题的判断

是否需要关注ABA?

场景 是否需要关注 原因
简单计数器 ❌ 不需要 只关心最终值,不关心中间过程
状态标志 ❌ 不需要 只有两个状态(true/false)
链表操作 ✅ 需要 指针变化会破坏数据结构
对象池 ✅ 需要 对象可能被回收再利用
金融交易 ✅ 需要 需要感知中间状态变化
版本控制 ✅ 需要 必须追踪历史变更

答题总结

核心要点

  1. 定义:ABA问题是CAS的经典陷阱,值从A变成B再变回A,CAS无法察觉中间状态

  2. 危害
    • 数据结构破坏(链表指针错乱)
    • 业务逻辑错误(余额变化未感知)
    • 内存安全问题(对象被回收再利用)
  3. 解决方案
    • AtomicStampedReference(版本号,精确追踪)
    • AtomicMarkableReference(布尔标记,简单场景)
    • 不可变对象(天然解决ABA)
    • 业务规避(通过状态机设计)
  4. 判断原则:不是所有CAS都需要关注ABA,简单计数器可以忽略

面试答题模板

“ABA问题是CAS操作的一个经典并发陷阱。指的是线程1读取变量值为A,准备CAS更新时,虽然变量值还是A,但实际上它已经被其他线程改为B再改回A,CAS无法察觉这个中间状态的变化。

这个问题在链表等数据结构操作中特别危险。比如链表头节点是A,线程1准备删除A,读取了A的next指针;这时线程2删除了A和B节点,又重新插入A;线程1的CAS会成功,但A的next指针已经变了,导致链表结构错误。

解决方案主要有四种:一是使用AtomicStampedReference,每次更新递增版本号,CAS时同时比较值和版本号;二是使用AtomicMarkableReference,通过布尔标记位标识状态变化;三是使用不可变对象,每次修改都创建新对象;四是在业务层面通过状态机设计规避。

不过要注意,并不是所有CAS都需要关注ABA。比如简单的计数器,只关心最终值不关心中间过程,就不需要处理ABA问题。”