CAS的ABA问题与解决方案深度解析
这是一个经典的并发编程面试题,考察对CAS机制、内存可见性、乐观锁设计的深度理解。下面我将从问题本质、解决方案、源码实现三个维度彻底讲透。
一、什么是ABA问题?
1. 问题定义
ABA问题是CAS(Compare-And-Swap)操作中的一种典型竞态条件:
-
线程1读取内存值A
-
线程2将值从A改为B,再改回A
-
线程1进行CAS操作,发现值仍然是A,操作成功
-
但实际上这个A已经不是原来的A了!
2. 问题重现代码
java
import java.util.concurrent.atomic.AtomicInteger;
public class ABAProblemDemo {
private static AtomicInteger counter = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
// 线程1:先读取值,然后做一些耗时操作
Thread t1 = new Thread(() -> {
int value = counter.get(); // 读取到100
System.out.println("线程1读取到值: " + value);
// 模拟耗时操作
try { Thread.sleep(2000); } catch (InterruptedException e) {}
// 尝试CAS更新:期望值100,新值200
boolean success = counter.compareAndSet(value, 200);
System.out.println("线程1 CAS操作结果: " + success + ", 当前值: " + counter.get());
});
// 线程2:快速修改两次
Thread t2 = new Thread(() -> {
// 第一次修改:100 → 150
counter.compareAndSet(100, 150);
System.out.println("线程2第一次修改: 100 → 150");
// 第二次修改:150 → 100(改回原值)
counter.compareAndSet(150, 100);
System.out.println("线程2第二次修改: 150 → 100");
});
t1.start();
Thread.sleep(100); // 确保t1先启动
t2.start();
t1.join();
t2.join();
System.out.println("\n最终结果分析:");
System.out.println("线程1以为值没变(还是100),实际上经历了 100→150→100 的变化");
System.out.println("这就是ABA问题!");
}
}
输出结果:
text
线程1读取到值: 100
线程2第一次修改: 100 → 150
线程2第二次修改: 150 → 100
线程1 CAS操作结果: true, 当前值: 200
最终结果分析:
线程1以为值没变(还是100),实际上经历了 100→150→100 的变化
这就是ABA问题!
3. ABA问题的危害场景
java
// 场景1:栈数据结构
public class Stack<T> {
private Node<T> top;
public void push(T value) {
Node<T> newHead = new Node<>(value);
Node<T> oldHead;
do {
oldHead = top; // 读取当前栈顶
newHead.next = oldHead;
} while (!casTop(oldHead, newHead)); // CAS更新栈顶
}
public T pop() {
Node<T> oldHead;
Node<T> newHead;
do {
oldHead = top; // 读取当前栈顶
if (oldHead == null) return null;
newHead = oldHead.next;
} while (!casTop(oldHead, newHead)); // CAS更新栈顶
return oldHead.value;
}
}
// ABA问题发生过程:
// 1. 线程A读取栈顶为Node1
// 2. 线程B弹出Node1,弹出Node2,再压入Node1(新对象,但值相同)
// 3. 线程A CAS成功,但Node1.next已经不是原来的Node2了!
// 结果:数据结构损坏!
二、解决方案:版本号/时间戳机制
1. AtomicStampedReference(JDK标准方案)
java
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolutionDemo {
// 初始值:100,初始版本号:0
private static AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 读取值和版本号
int[] stampHolder = new int[1];
int value = atomicRef.get(stampHolder);
int oldStamp = stampHolder[0];
System.out.println("线程1读取: 值=" + value + ", 版本=" + oldStamp);
try { Thread.sleep(2000); } catch (InterruptedException e) {}
// CAS操作:同时检查值和版本号
boolean success = atomicRef.compareAndSet(
value, // 期望值
200, // 新值
oldStamp, // 期望版本号
oldStamp + 1 // 新版本号
);
System.out.println("线程1 CAS结果: " + success +
", 当前值=" + atomicRef.getReference() +
", 版本=" + atomicRef.getStamp());
});
Thread t2 = new Thread(() -> {
// 第一次修改
int[] stampHolder = new int[1];
int currentStamp = atomicRef.getStamp();
atomicRef.compareAndSet(100, 150, currentStamp, currentStamp + 1);
System.out.println("线程2第一次修改: 100→150, 版本" + currentStamp + "→" + (currentStamp + 1));
// 第二次修改(改回原值,但版本号增加)
currentStamp = atomicRef.getStamp();
atomicRef.compareAndSet(150, 100, currentStamp, currentStamp + 1);
System.out.println("线程2第二次修改: 150→100, 版本" + currentStamp + "→" + (currentStamp + 1));
});
t1.start();
Thread.sleep(100);
t2.start();
t1.join();
t2.join();
System.out.println("\n最终:线程1的CAS操作失败!因为版本号已变化。");
}
}
源码分析:
java
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp; // 版本号
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
}
2. AtomicMarkableReference(简化版)
java
import java.util.concurrent.atomic.AtomicMarkableReference;
public class MarkableReferenceDemo {
// 初始值:100,标记:false
private static AtomicMarkableReference<Integer> atomicRef =
new AtomicMarkableReference<>(100, false);
public static void main(String[] args) {
// 获取值和标记
boolean[] markHolder = new boolean[1];
int value = atomicRef.get(markHolder);
boolean oldMark = markHolder[0];
// CAS操作:同时检查值和标记
boolean success = atomicRef.compareAndSet(
value, 200,
oldMark, !oldMark // 修改标记
);
System.out.println("CAS结果: " + success);
}
}
适用场景:
-
AtomicStampedReference:需要完整版本历史 -
AtomicMarkableReference:只需知道是否被修改过(如GC标记)
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
三、数据库领域的ABA问题解决
1. 乐观锁实现(版本号方案)
sql
-- 数据库表设计
CREATE TABLE user_balance (
id BIGINT PRIMARY KEY,
user_id BIGINT,
balance DECIMAL(10,2),
version INT DEFAULT 0, -- 版本号字段
update_time TIMESTAMP
);
-- 更新操作(Java + MyBatis)
public boolean updateBalance(Long userId, BigDecimal amount) {
UserBalance current = userBalanceMapper.selectForUpdate(userId);
// 模拟CAS:版本号检查
int rows = userBalanceMapper.updateWithVersion(
userId,
current.getBalance().add(amount),
current.getVersion(), // 期望版本号
current.getVersion() + 1 // 新版本号
);
return rows > 0; // rows>0表示成功,否则被其他事务修改
}
-- MyBatis映射
<update id="updateWithVersion">
UPDATE user_balance
SET balance = #{newBalance},
version = #{newVersion},
update_time = NOW()
WHERE user_id = #{userId}
AND version = #{oldVersion} -- CAS条件
</update>
2. 时间戳方案
sql
CREATE TABLE product_stock (
product_id BIGINT PRIMARY KEY,
stock INT,
update_time TIMESTAMP(6) -- 微秒级时间戳
);
-- 更新时检查时间戳
UPDATE product_stock
SET stock = stock - 1,
update_time = CURRENT_TIMESTAMP(6)
WHERE product_id = #{productId}
AND update_time = #{oldTimestamp} -- 精确到微秒
AND stock > 0;
四、分布式系统中的ABA问题
1. 分布式锁的实现
java
public class DistributedLockWithToken {
private final RedisTemplate redisTemplate;
public boolean tryLock(String key, int expireSeconds) {
// 使用UUID + 时间戳作为token(版本号)
String token = UUID.randomUUID().toString() + ":" + System.currentTimeMillis();
// SET key token NX EX expireSeconds
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, token, expireSeconds, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)) {
// 保存token到ThreadLocal,用于后续CAS释放
tokenHolder.set(token);
return true;
}
return false;
}
public boolean unlock(String key) {
String currentToken = tokenHolder.get();
if (currentToken == null) return false;
// 使用Lua脚本保证原子性
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
currentToken
);
return result != null && result > 0;
}
}
2. 分布式ID生成器的ABA防护
java
public class DistributedIdGenerator {
// 雪花算法 + 序列号重置防护
private long lastTimestamp = -1L;
private long sequence = 0L;
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨!");
}
if (timestamp == lastTimestamp) {
// 同一毫秒内
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
// 序列号溢出,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 新的一毫秒,序列号重置
sequence = 0L; // ❌ 这里可能有ABA问题!
}
// 解决方案:加入机器ID或数据中心ID作为版本号
long id = ((timestamp - TWEPOCH) << TIMESTAMP_SHIFT)
| (dataCenterId << DATACENTER_SHIFT)
| (workerId << WORKER_SHIFT)
| sequence;
lastTimestamp = timestamp;
return id;
}
}
五、硬件层面的解决方案
1. LL/SC(Load-Link/Store-Conditional)
assembly
# ARM架构的LL/SC指令示例
loop:
LDREX R1, [R0] # Load-Link: 加载值并建立监控
ADD R1, R1, #1 # 计算新值
STREX R2, R1, [R0] # Store-Conditional: 如果内存未被修改则存储
CMP R2, #0 # 检查是否成功
BNE loop # 失败则重试
原理:
-
LDREX:加载值并标记该内存区域 -
期间如果有其他处理器修改该内存,标记被清除
-
STREX:检查标记,如果还在则存储,否则失败
2. 双字CAS(DCAS)
c
// x86的CMPXCHG16B指令(128位CAS)
bool double_word_cas(__int128* ptr, __int128 old_val, __int128 new_val) {
// 高64位:版本号
// 低64位:实际值
return __sync_bool_compare_and_swap(ptr, old_val, new_val);
}
六、面试深度回答要点
基础回答(校招/初级)
"ABA问题是CAS操作中的一个典型问题:一个值从A变成B又变回A,CAS检查时发现值没变就操作成功,但实际上中间状态已经改变。可以用AtomicStampedReference通过版本号解决。"
进阶回答(社招/中级)
"ABA问题的本质是CAS只检查值相等,不检查状态连续性。解决方案有:1) 版本号机制(AtomicStampedReference);2) 标记位机制(AtomicMarkableReference);3) 数据库乐观锁;4) 硬件LL/SC指令。在分布式系统中,可以通过token或时间戳防止ABA问题。"
深度回答(高级/架构师)
"这本质上是状态机 的版本管理问题。CAS只能判断值相等,不能判断状态连续性。解决方案的核心是引入单调递增的版本号,将一维的值比较升级为二维的(值,版本)比较。在数据库领域,这演化为乐观锁;在分布式系统,这演化为token机制;在硬件层面,LL/SC提供了更优雅的实现。实际应用中,我们还需要考虑版本号溢出、分布式时钟同步等问题。"
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
高频扩展问题
-
AtomicStampedReference的实现原理?
-
内部维护一个Pair对象,包含reference和stamp
-
CAS时同时比较reference和stamp
-
-
版本号会溢出吗?如何解决?
-
会,int类型最多42亿次
-
解决方案:使用AtomicStampedReference时,到达最大值后重置或抛异常
-
-
数据库乐观锁和CAS有什么区别?
-
本质相同:都是乐观并发控制
-
实现不同:数据库在SQL层面,CAS在内存层面
-
粒度不同:数据库是行级,CAS是变量级
-
-
ABA问题在实际业务中真的有害吗?
-
栈/队列数据结构:绝对有害,会导致数据结构损坏
-
计数器场景:可能无害,如果只关心最终值
-
需要根据业务语义判断
-
-
除了版本号,还有其他方案吗?
-
Hazard Pointer(危险指针)
-
RCU(Read-Copy-Update)
-
事务内存(Transactional Memory)
-
掌握ABA问题的本质和解决方案,不仅能通过面试,更能帮助你在实际开发中设计出更健壮的并发程序。