什么是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) |
| 链表操作 | ✅ 需要 | 指针变化会破坏数据结构 |
| 对象池 | ✅ 需要 | 对象可能被回收再利用 |
| 金融交易 | ✅ 需要 | 需要感知中间状态变化 |
| 版本控制 | ✅ 需要 | 必须追踪历史变更 |
答题总结
核心要点
-
定义:ABA问题是CAS的经典陷阱,值从A变成B再变回A,CAS无法察觉中间状态
- 危害:
- 数据结构破坏(链表指针错乱)
- 业务逻辑错误(余额变化未感知)
- 内存安全问题(对象被回收再利用)
- 解决方案:
- AtomicStampedReference(版本号,精确追踪)
- AtomicMarkableReference(布尔标记,简单场景)
- 不可变对象(天然解决ABA)
- 业务规避(通过状态机设计)
- 判断原则:不是所有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问题。”