CAS的ABA问题:看似未变实则沧海桑田🔄

73 阅读9分钟

你以为她还是你认识的那个她,其实她已经经历了无数故事,只是又回到了原点。这就是ABA问题!

一、开场:CAS的魔法与陷阱🎩

CAS是什么?

CAS = Compare And Swap(比较并交换)

// 伪代码
boolean compareAndSwap(int expectedValue, int newValue) {
    if (currentValue == expectedValue) {
        currentValue = newValue;
        return true; // 成功
    }
    return false; // 失败,有其他线程修改了
}

原子操作: 比较和交换是一条CPU指令,不会被中断。

生活类比:

你去超市买牛奶🥛:

  1. 看到货架上有一瓶"蒙牛"(expected)
  2. 拿走它,放上一瓶"伊利"(new value)
  3. 如果货架上还是"蒙牛",替换成功;否则失败(有人抢先了)

二、ABA问题:时光倒流的假象⏰

问题场景

时刻1: 线程1读取 value = A
时刻2: 线程2将 A → B
时刻3: 线程2将 B → A (改回去了!)
时刻4: 线程1执行CAS:期望A,当前也是A,成功!

问题: 线程1以为value一直是A,但实际上经历了A → B → A

代码演示

public class ABADemo {
    private static AtomicInteger value = new AtomicInteger(100);
    
    public static void main(String[] args) throws InterruptedException {
        
        // 线程1:取钱
        Thread t1 = new Thread(() -> {
            int expected = value.get(); // 读到100
            System.out.println("线程1读取余额:" + expected);
            
            // 模拟耗时操作
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // CAS:期望100,改为80(取20元)
            boolean success = value.compareAndSet(expected, 80);
            System.out.println("线程1取钱:" + (success ? "成功" : "失败"));
        });
        
        // 线程2:转账
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(100); // 稍后执行
                
                // 100 → 120(转入20)
                value.compareAndSet(100, 120);
                System.out.println("线程2转入20,余额:" + value.get());
                
                Thread.sleep(100);
                
                // 120 → 100(转出20)
                value.compareAndSet(120, 100);
                System.out.println("线程2转出20,余额:" + value.get());
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("最终余额:" + value.get());
    }
}

输出:

线程1读取余额:100
线程2转入20,余额:120
线程2转出20,余额:100
线程1取钱:成功
最终余额:80

问题分析:

线程1在睡眠期间,余额经历了100 → 120 → 100,但线程1的CAS还是成功了,因为期望值(100)和当前值(100)相等!

这就是ABA问题!


三、ABA问题的危害🚨

案例1:栈的并发操作

单向链表实现的栈:

class Node {
    int value;
    Node next;
}

class Stack {
    private AtomicReference<Node> top = new AtomicReference<>();
    
    public void push(int value) {
        Node newNode = new Node(value);
        Node oldTop;
        do {
            oldTop = top.get();
            newNode.next = oldTop;
        } while (!top.compareAndSet(oldTop, newNode));
    }
    
    public Integer pop() {
        Node oldTop;
        Node newTop;
        do {
            oldTop = top.get();
            if (oldTop == null) return null;
            newTop = oldTop.next;
        } while (!top.compareAndSet(oldTop, newTop));
        
        return oldTop.value;
    }
}

危险场景:

初始栈: topABC

时刻1: 线程1准备pop,读到 oldTop = A, newTop = B
时刻2: 线程2 pop AtopB)
时刻3: 线程2 pop BtopC)
时刻4: 线程2 push AtopACAnext指向C!)
时刻5: 线程1执行CAS:期望top=A,当前top=A,成功!
       → top = BBnext还指向原来的C)

结果:C丢失了!内存泄漏!

可视化:

原始:  topABC

线程2操作后:
       topACB (悬空,被回收)

线程1 CAS后:
       topB → ??? (B的next指向已回收的内存!)

案例2:内存池的ABA

class MemoryPool {
    private AtomicReference<Block> freeList;
    
    public Block allocate() {
        Block oldHead;
        Block newHead;
        do {
            oldHead = freeList.get();
            if (oldHead == null) return allocateNew();
            newHead = oldHead.next;
        } while (!freeList.compareAndSet(oldHead, newHead));
        
        return oldHead;
    }
    
    public void free(Block block) {
        Block oldHead;
        do {
            oldHead = freeList.get();
            block.next = oldHead;
        } while (!freeList.compareAndSet(oldHead, block));
    }
}

ABA问题:

  1. 线程1分配Block A,读到freeList
  2. 线程2分配A,再释放A(A又回到freeList)
  3. 线程1的CAS成功,但A可能已经被修改过!

四、解决方案1:AtomicStampedReference(版本号)⭐

核心思想

每次修改都递增版本号,即使值相同,版本号也不同!

// 不仅比较值,还比较版本号
boolean compareAndSet(
    V expectedReference,    // 期望的值
    V newReference,        // 新值
    int expectedStamp,     // 期望的版本号
    int newStamp          // 新版本号
)

代码示例

public class AtomicStampedReferenceDemo {
    
    public static void main(String[] args) throws InterruptedException {
        
        // 初始值:100,版本号:1
        AtomicStampedReference<Integer> asr = 
            new AtomicStampedReference<>(100, 1);
        
        // 线程1:取钱
        Thread t1 = new Thread(() -> {
            int[] stampHolder = new int[1];
            Integer value = asr.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,期望版本1,新值80,新版本2
            boolean success = asr.compareAndSet(value, 80, stamp, stamp + 1);
            System.out.println("线程1 CAS:" + (success ? "成功" : "失败"));
        });
        
        // 线程2:ABA操作
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(100);
                
                // 100 → 120, 版本1 → 2
                int[] stampHolder = new int[1];
                Integer value = asr.get(stampHolder);
                asr.compareAndSet(value, 120, stampHolder[0], stampHolder[0] + 1);
                System.out.println("线程2:100 → 120, stamp=" + (stampHolder[0] + 1));
                
                Thread.sleep(100);
                
                // 120 → 100, 版本2 → 3
                value = asr.get(stampHolder);
                asr.compareAndSet(value, 100, stampHolder[0], stampHolder[0] + 1);
                System.out.println("线程2:120 → 100, stamp=" + (stampHolder[0] + 1));
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("最终:value=" + asr.getReference() + 
                          ", stamp=" + asr.getStamp());
    }
}

输出:

线程1读取:value=100, stamp=1
线程2:100 → 120, stamp=2
线程2:120 → 100, stamp=3
线程1 CAS:失败  ← 版本号不匹配!
最终:value=100, stamp=3

关键点:

  • 线程1期望版本号=1
  • 实际版本号=3(经历了两次修改)
  • CAS失败,避免了ABA问题!

完整API

AtomicStampedReference<V> asr = new AtomicStampedReference<>(initialValue, initialStamp);

// 获取值和版本号
int[] stampHolder = new int[1];
V value = asr.get(stampHolder);
int stamp = stampHolder[0];

// CAS操作
boolean success = asr.compareAndSet(
    expectedValue, newValue,
    expectedStamp, newStamp
);

// 只获取值
V ref = asr.getReference();

// 只获取版本号
int currentStamp = asr.getStamp();

// 强制设置(不管当前值)
asr.set(newValue, newStamp);

// 尝试原子更新版本号(值不变)
boolean updated = asr.attemptStamp(expectedValue, newStamp);

五、解决方案2:AtomicMarkableReference(标记)🏷️

核心思想

不关心修改了多少次,只关心是否修改过。

boolean compareAndSet(
    V expectedReference,
    V newReference,
    boolean expectedMark,   // 期望的标记
    boolean newMark        // 新标记
)

适用场景: 只需要知道"是否被动过",不需要知道动了几次。

代码示例

public class AtomicMarkableReferenceDemo {
    
    public static void main(String[] args) {
        
        // 初始值:100,标记:false(未修改)
        AtomicMarkableReference<Integer> amr = 
            new AtomicMarkableReference<>(100, false);
        
        // 线程1
        new Thread(() -> {
            boolean[] markHolder = new boolean[1];
            Integer value = amr.get(markHolder);
            boolean mark = markHolder[0];
            
            System.out.println("线程1读取:value=" + value + ", mark=" + mark);
            
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // CAS:期望mark=false
            boolean success = amr.compareAndSet(value, 80, mark, true);
            System.out.println("线程1 CAS:" + (success ? "成功" : "失败"));
        }).start();
        
        // 线程2:修改后标记
        new Thread(() -> {
            try {
                Thread.sleep(100);
                
                boolean[] markHolder = new boolean[1];
                Integer value = amr.get(markHolder);
                
                // 修改值,标记为true
                amr.compareAndSet(value, 120, markHolder[0], true);
                System.out.println("线程2修改,mark=true");
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

输出:

线程1读取:value=100, mark=false
线程2修改,mark=true
线程1 CAS:失败  ← 标记不匹配

Markable vs Stamped

特性AtomicMarkableReferenceAtomicStampedReference
版本控制boolean标记(修改/未修改)int版本号(修改次数)
精度低(只知道改没改)高(知道改了几次)
内存小(1bit)大(32bit)
适用场景简单的修改检测严格的ABA防护

六、解决方案3:不可变对象🔒

核心思想

每次修改都创建新对象,而不是修改原对象。

class Account {
    private final int balance;
    
    public Account(int balance) {
        this.balance = balance;
    }
    
    public int getBalance() {
        return balance;
    }
    
    // 返回新对象
    public Account withdraw(int amount) {
        return new Account(balance - amount);
    }
    
    public Account deposit(int amount) {
        return new Account(balance + amount);
    }
}

// 使用AtomicReference
class BankAccount {
    private AtomicReference<Account> account = 
        new AtomicReference<>(new Account(100));
    
    public void withdraw(int amount) {
        Account oldAccount;
        Account newAccount;
        do {
            oldAccount = account.get();
            newAccount = oldAccount.withdraw(amount);
        } while (!account.compareAndSet(oldAccount, newAccount));
    }
}

优势:

  • 对象不可变,线程安全
  • 不会有ABA问题(每次都是新对象)

劣势:

  • 创建对象有开销
  • GC压力

七、解决方案4:业务上避免ABA🎯

方案1:增加约束条件

// 不仅比较余额,还比较其他条件
class SmartAccount {
    private AtomicInteger balance = new AtomicInteger(100);
    private AtomicLong lastModifyTime = new AtomicLong(System.currentTimeMillis());
    
    public boolean withdraw(int amount, long expectedTime) {
        int oldBalance = balance.get();
        long oldTime = lastModifyTime.get();
        
        // 检查时间和余额
        if (oldTime != expectedTime) {
            return false; // 有人修改过
        }
        
        return balance.compareAndSet(oldBalance, oldBalance - amount) &&
               lastModifyTime.compareAndSet(oldTime, System.currentTimeMillis());
    }
}

方案2:使用悲观锁

// 如果ABA很难避免,就用锁
class LockedAccount {
    private int balance = 100;
    private final ReentrantLock lock = new ReentrantLock();
    
    public void withdraw(int amount) {
        lock.lock();
        try {
            if (balance >= amount) {
                balance -= amount;
            }
        } finally {
            lock.unlock();
        }
    }
}

什么时候用锁?

  • ABA问题严重且难以解决
  • 性能要求不高
  • 逻辑复杂,CAS难以实现

八、实战:无锁栈的ABA修复🛠️

有问题的版本

class LockFreeStack<T> {
    private AtomicReference<Node<T>> top = new AtomicReference<>();
    
    static class Node<T> {
        T value;
        Node<T> next;
        
        Node(T value) {
            this.value = value;
        }
    }
    
    public void push(T value) {
        Node<T> newNode = new Node<>(value);
        Node<T> oldTop;
        do {
            oldTop = top.get();
            newNode.next = oldTop;
        } while (!top.compareAndSet(oldTop, newNode));
    }
    
    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));
        
        return oldTop.value;
    }
}

修复版本:使用版本号

class SafeLockFreeStack<T> {
    private AtomicStampedReference<Node<T>> top = 
        new AtomicStampedReference<>(null, 0);
    
    static class Node<T> {
        T value;
        Node<T> next;
        
        Node(T value) {
            this.value = value;
        }
    }
    
    public void push(T value) {
        Node<T> newNode = new Node<>(value);
        int[] stampHolder = new int[1];
        Node<T> oldTop;
        
        do {
            oldTop = top.get(stampHolder);
            int stamp = stampHolder[0];
            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;
            int stamp = stampHolder[0];
            newTop = oldTop.next;
        } while (!top.compareAndSet(oldTop, newTop, stamp, stamp + 1));
        
        return oldTop.value;
    }
}

九、性能对比测试📊

public class PerformanceTest {
    
    private static final int THREADS = 10;
    private static final int OPERATIONS = 1_000_000;
    
    public static void main(String[] args) throws InterruptedException {
        
        // 测试1:AtomicInteger(可能有ABA)
        testAtomicInteger();
        
        // 测试2:AtomicStampedReference
        testAtomicStamped();
        
        // 测试3:AtomicMarkableReference
        testAtomicMarkable();
        
        // 测试4:ReentrantLock
        testLock();
    }
    
    private static void testAtomicInteger() throws InterruptedException {
        AtomicInteger counter = new AtomicInteger(0);
        long start = System.currentTimeMillis();
        
        Thread[] threads = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < OPERATIONS; j++) {
                    counter.incrementAndGet();
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        
        long time = System.currentTimeMillis() - start;
        System.out.println("AtomicInteger: " + time + "ms");
    }
    
    private static void testAtomicStamped() throws InterruptedException {
        AtomicStampedReference<Integer> counter = 
            new AtomicStampedReference<>(0, 0);
        long start = System.currentTimeMillis();
        
        Thread[] threads = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < OPERATIONS; j++) {
                    int[] stampHolder = new int[1];
                    Integer value;
                    do {
                        value = counter.get(stampHolder);
                    } while (!counter.compareAndSet(value, value + 1, 
                                                    stampHolder[0], stampHolder[0] + 1));
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        
        long time = System.currentTimeMillis() - start;
        System.out.println("AtomicStampedReference: " + time + "ms");
    }
}

典型结果:

AtomicInteger: 1200ms
AtomicStampedReference: 3500ms (慢3倍)
AtomicMarkableReference: 3200ms
ReentrantLock: 2000ms

结论:

  • AtomicInteger最快,但可能有ABA
  • Stamped/Markable更安全,但性能较差
  • Lock居中,使用简单

十、什么时候需要担心ABA?⚠️

需要担心的场景

  1. 链表/栈/队列的并发操作

    • 节点被移除后又添加回来
    • 指针指向已释放内存
  2. 内存管理

    • 内存块被回收后又分配
    • 野指针问题
  3. 版本敏感的业务

    • 需要知道数据是否被修改过
    • 即使改回原值也算修改

不需要担心的场景

  1. 简单计数器

    • counter++,只关心结果
  2. 状态标志

    • boolean flag,只有两个状态
  3. 纯数值运算

    • 加减乘除,不涉及对象引用

十一、面试高频问答💯

Q1: 什么是ABA问题?

A: CAS操作时,值从A变为B又变回A,导致CAS误以为没有被修改过,实际上已经经历了变化。

Q2: ABA问题有什么危害?

A:

  • 链表结构可能出现野指针
  • 内存管理可能重复回收
  • 业务逻辑可能基于错误的假设

Q3: 如何解决ABA问题?

A:

  • AtomicStampedReference(版本号)
  • AtomicMarkableReference(标记位)
  • 不可变对象
  • 业务上增加约束
  • 使用锁

Q4: AtomicStampedReference的性能如何?

A: 比AtomicInteger慢2-3倍,因为要同时CAS两个字段(值和版本号)。

Q5: 所有CAS场景都需要解决ABA吗?

A: 不需要! 大部分场景(如计数器)不受ABA影响。只有涉及对象引用和内存管理时才需要担心。


十二、总结:ABA问题应对指南🎯

决策树

使用CAS吗?
├─ 简单数值运算 → 用AtomicInteger/Long ✅
├─ 对象引用操作
│  ├─ 链表/栈/队列 → 用AtomicStampedReference ⚠️
│  ├─ 只需检测修改 → 用AtomicMarkableReference
│  └─ 逻辑复杂 → 考虑用Lock 🔒
└─ 不确定 → 先用Lock,有性能瓶颈再优化

最佳实践

  1. 优先用Atomic原子类(简单场景)
  2. 谨慎处理对象引用(链表/内存池)
  3. 必要时加版本号(AtomicStamped)
  4. 性能不够再优化(不要过早优化)
  5. 测试并发场景(压力测试)

下期预告: LongAdder如何通过"分治思想"吊打AtomicLong?高并发计数的终极秘密!🔢