news 2026/2/16 22:15:44

HSBC Java面试被问:CAS如何解决ABA问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HSBC Java面试被问:CAS如何解决ABA问题

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

需要全套面试笔记及答案
【点击此处即可/免费获取】​​​

高频扩展问题

  1. AtomicStampedReference的实现原理?

    • 内部维护一个Pair对象,包含reference和stamp

    • CAS时同时比较reference和stamp

  2. 版本号会溢出吗?如何解决?

    • 会,int类型最多42亿次

    • 解决方案:使用AtomicStampedReference时,到达最大值后重置或抛异常

  3. 数据库乐观锁和CAS有什么区别?

    • 本质相同:都是乐观并发控制

    • 实现不同:数据库在SQL层面,CAS在内存层面

    • 粒度不同:数据库是行级,CAS是变量级

  4. ABA问题在实际业务中真的有害吗?

    • 栈/队列数据结构:绝对有害,会导致数据结构损坏

    • 计数器场景:可能无害,如果只关心最终值

    • 需要根据业务语义判断

  5. 除了版本号,还有其他方案吗?

    • Hazard Pointer(危险指针)

    • RCU(Read-Copy-Update)

    • 事务内存(Transactional Memory)

掌握ABA问题的本质和解决方案,不仅能通过面试,更能帮助你在实际开发中设计出更健壮的并发程序。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!