你以为她还是你认识的那个她,其实她已经经历了无数故事,只是又回到了原点。这就是ABA问题!
一、开场:CAS的魔法与陷阱🎩
CAS是什么?
CAS = Compare And Swap(比较并交换)
// 伪代码
boolean compareAndSwap(int expectedValue, int newValue) {
if (currentValue == expectedValue) {
currentValue = newValue;
return true; // 成功
}
return false; // 失败,有其他线程修改了
}
原子操作: 比较和交换是一条CPU指令,不会被中断。
生活类比:
你去超市买牛奶🥛:
- 看到货架上有一瓶"蒙牛"(expected)
- 拿走它,放上一瓶"伊利"(new value)
- 如果货架上还是"蒙牛",替换成功;否则失败(有人抢先了)
二、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;
}
}
危险场景:
初始栈: top → A → B → C
时刻1: 线程1准备pop,读到 oldTop = A, newTop = B
时刻2: 线程2 pop A(top → B)
时刻3: 线程2 pop B(top → C)
时刻4: 线程2 push A(top → A → C,A的next指向C!)
时刻5: 线程1执行CAS:期望top=A,当前top=A,成功!
→ top = B(B的next还指向原来的C)
结果:C丢失了!内存泄漏!
可视化:
原始: top → A → B → C
线程2操作后:
top → A → C
↑
B (悬空,被回收)
线程1 CAS后:
top → B → ??? (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分配Block A,读到freeList
- 线程2分配A,再释放A(A又回到freeList)
- 线程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
| 特性 | AtomicMarkableReference | AtomicStampedReference |
|---|---|---|
| 版本控制 | 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?⚠️
需要担心的场景
-
链表/栈/队列的并发操作
- 节点被移除后又添加回来
- 指针指向已释放内存
-
内存管理
- 内存块被回收后又分配
- 野指针问题
-
版本敏感的业务
- 需要知道数据是否被修改过
- 即使改回原值也算修改
不需要担心的场景
-
简单计数器
counter++,只关心结果
-
状态标志
boolean flag,只有两个状态
-
纯数值运算
- 加减乘除,不涉及对象引用
十一、面试高频问答💯
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,有性能瓶颈再优化
最佳实践
- 优先用Atomic原子类(简单场景)
- 谨慎处理对象引用(链表/内存池)
- 必要时加版本号(AtomicStamped)
- 性能不够再优化(不要过早优化)
- 测试并发场景(压力测试)
下期预告: LongAdder如何通过"分治思想"吊打AtomicLong?高并发计数的终极秘密!🔢