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问题的本质和解决方案,不仅能通过面试,更能帮助你在实际开发中设计出更健壮的并发程序。

相关推荐
AI人工智能+电脑小能手6 小时前
【大白话说Java面试题 第87题】【Mysql篇】第17题:分布式事务的实现原理?
java·数据库·分布式·mysql·面试
来杯@Java7 小时前
图书管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·mybatis·课程设计
卷毛的技术笔记8 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
编程大师哥8 小时前
匿名函数 lambda + 高阶函数
java·python·算法
isyangli_blog8 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008118 小时前
FastAPI APIRouter
开发语言·python
Benszen8 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆8 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木8 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
Cosolar8 小时前
从零写一个 Attention Is All You Need
人工智能·面试·架构