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

相关推荐
lang201509282 小时前
深入解析Java资源加载机制
java·开发语言·python
爱笑的眼睛113 小时前
自动机器学习组件的深度解析:超越AutoML框架的底层架构
java·人工智能·python·ai
LCG米3 小时前
嵌入式Python工业环境监测实战:MicroPython读取多传感器数据
开发语言·人工智能·python
⑩-3 小时前
简单业务异常类
java
乘风!3 小时前
NSSM启动tomcat部署Java程序
java·服务器·后端·tomcat
BBB努力学习程序设计3 小时前
Java 21虚拟线程与平台线程:JVM层面的深度对比与实现原理
java
代码无疆3 小时前
学点java字节码更易于理解一些特殊的java语法效果
java·后端
BBB努力学习程序设计3 小时前
Java 8日期时间API完全指南:告别Date和Calendar的混乱时代
java
uhakadotcom3 小时前
Python Protobuf 全面教程:常用 API 串联与实战指南
前端·面试·github