更合适的例子:无锁栈(Treiber Stack)的 ABA 问题
ABA 问题在 无锁数据结构 (如栈、队列)中更常见,而余额修改场景确实不太典型。我们换一个更合适的例子:无锁栈的 ABA 问题。
1. 问题场景:无锁栈(Treiber Stack)
实现一个线程安全的栈 ,使用 AtomicReference
管理栈顶:
java
import java.util.concurrent.atomic.AtomicReference;
class ConcurrentStack<T> {
private static class Node<T> {
final T value;
Node<T> next;
Node(T value) {
this.value = value;
}
}
private final AtomicReference<Node<T>> top = new AtomicReference<>();
public void push(T item) {
Node<T> newNode = new Node<>(item);
Node<T> oldTop;
do {
oldTop = top.get();
newNode.next = oldTop;
} while (!top.compareAndSet(oldTop, newNode)); // CAS 更新栈顶
}
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)); // CAS 更新栈顶
return oldTop.value;
}
}
2. ABA 问题如何发生?
假设栈初始状态:
css
top -> A -> B -> C
线程 1 执行 pop()
:
- 读取
oldTop = A
,准备 CAS 更新栈顶为B
。 - 被挂起(还未执行 CAS)。
线程 2 执行两次 pop()
:
- 弹出
A
,栈变为B -> C
。 - 弹出
B
,栈变为C
。 - 再
push(A)
,栈变回A -> C
。
线程 1 恢复执行 : • 执行 top.compareAndSet(A, B)
,发现 top
仍然是 A
(ABA 发生!)。 • CAS 错误地成功 ,导致栈顶被设为 B
(但 B
已经被弹出过,可能已被其他线程修改)。
最终栈状态:
css
top -> B // 但 B 的 next 可能已经失效(如指向一个已删除的节点)
问题 :
• 栈结构被破坏,可能导致后续操作出错(如 NullPointerException
)。 • 即使值看起来没变(A
→ B
→ A
),但中间状态已被篡改。
3. 解决方案:AtomicStampedReference
通过 版本号(Stamp) 跟踪栈顶变化:
java
import java.util.concurrent.atomic.AtomicStampedReference;
class SafeConcurrentStack<T> {
private static class Node<T> {
final T value;
Node<T> next;
Node(T value) {
this.value = value;
}
}
private final AtomicStampedReference<Node<T>> top =
new AtomicStampedReference<>(null, 0);
public void push(T item) {
Node<T> newNode = new Node<>(item);
int[] stampHolder = new int[1];
Node<T> oldTop;
do {
oldTop = top.get(stampHolder); // 获取当前栈顶和版本号
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;
}
newTop = oldTop.next;
} while (!top.compareAndSet(oldTop, newTop, stampHolder[0], stampHolder[0] + 1));
return oldTop.value;
}
}
关键改进 : • 每次修改栈顶时,版本号(Stamp
)递增。 • CAS 不仅比较引用,还比较版本号,确保中间未被其他线程修改。
4. 为什么余额修改不需要担心 ABA?
在余额修改场景(如 AtomicInteger
):
java
AtomicInteger balance = new AtomicInteger(100);
// 线程1:读取 balance=100,准备扣款 50(预期余额 100)
int oldBalance = balance.get();
int newBalance = oldBalance - 50;
// 线程2:临时修改 balance 为 200,又改回 100
balance.set(200);
balance.set(100);
// 线程1:执行 CAS(100 → 50)
balance.compareAndSet(100, 50); // 仍然成功
为什么没问题?
• 余额从 100
→ 200
→ 100
,虽然值相同,但业务逻辑上 没有副作用 : • 只要最终余额正确(100 - 50 = 50
),中间变化不影响结果。 • ABA 问题仅在数据结构依赖中间状态时才有害(如栈的节点指针被篡改)。
5. 总结
场景 | 是否需要解决 ABA 问题? | 推荐工具 |
---|---|---|
无锁栈/队列 | ✅ 需要 | AtomicStampedReference |
余额修改(单变量) | ❌ 不需要 | AtomicInteger |
状态机/版本控制 | ✅ 需要 | AtomicStampedReference |
关键结论
- ABA 问题在无锁数据结构中更严重(如栈、队列、链表),因为指针可能被篡改。
- 余额修改等单变量操作通常不受影响,因为只关心最终值。
AtomicStampedReference
是解决 ABA 问题的标准方案,通过版本号跟踪变化。