在这里插入图片描述

问题

在多线程高并发场景下,CAS (Compare And Swap,比较并交换) 是实现无锁化编程的核心基石。它利用 CPU 的原子指令(如 cmpxchg)实现了极高性能的并发更新。然而,CAS 并非完美,其中最经典的挑战便是 ABA 问题

本篇博客将深入探讨 ABA 问题的本质、为何不能使用“加锁”来解决,以及 Java 如何通过 AtomicStampedReference 优雅地化解这一难题。


一、 什么是 ABA 问题?

CAS 的逻辑是:“我认为值应该是 A,如果是,则更新为 B。”

ABA 现象描述:

  1. 线程 T1 读取共享变量值为 A
  2. 线程 T2 介入,将值从 A 改为 B,随后又将其从 B 改回 A
  3. 线程 T1 恢复执行,进行 CAS 操作。它发现值依然是 A,于是判定“没有变化”,更新成功。

潜在风险:
虽然数值上看起来没变,但变量的状态或引用指向的内容可能已经发生了本质变化。在某些数据结构(如无锁栈或链表)中,这可能导致指针悬挂或逻辑混乱。


二、 为什么不能通过“加锁”来解决?

面对并发冲突,最简单的直觉是加锁(synchronizedReentrantLock)。但在 CAS 场景下,这与无锁的初衷背道而驰:

  1. 违背设计初衷:CAS 存在的意义就是为了无锁化,避免内核态切换、上下文切换和线程阻塞带来的巨大开销。
  2. 性能瓶颈:加锁会将并发操作变为串行,在高竞争环境下,锁的开销远大于 CAS 自旋
  3. 复杂度增加:锁可能引入死锁、活锁等风险。

结论: 解决 CAS 的问题,必须坚持使用乐观锁的思想,在“无锁”的框架内寻找方案。


三、 核心方案:引入版本号

解决 ABA 的标准思想是:“不仅比较值,还要比较版本号。”

我们为变量额外维护一个单调递增的版本号(或时间戳)。

  • 判定逻辑(当前值 == 预期值) && (当前版本号 == 预期版本号)
  • 即使值从 A 变回了 A,版本号也会从 1 变为 3,CAS 依然会检测到变化并拦截。

四、 推荐实现:AtomicStampedReference

在 Java 中,我们不需要手动通过 AtomicReference + AtomicInteger 来维护这种关系。因为手动维护无法保证“值+版本号”两者同时更新的原子性!!!

Java 并发包提供了 AtomicStampedReference<V>,其底层设计极其精妙:

1. 内部类 Pair

它将实际引用 reference 和版本戳 stamp 封装在一个静态内部类 Pair 中:

在这里插入图片描述

2. CAS 对象引用

AtomicStampedReference 内部维护一个 volatile Pair<V> pair 对象。
它的 CAS 操作实际上是Pair 对象引用的整体替换。由于替换一个对象的引用是原子指令,从而实现了“值+版本号”的联合原子更新


五、 实战演示:拦截 ABA 过程

以下代码演示了如何利用 AtomicStampedReference 识别并拦截 ABA 问题:

import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 演示:使用 AtomicStampedReference 解决 CAS 的 ABA 问题
 */
public class ABASolution {
    public static void main(String[] args) {
        // 初始化:值为"A",初始版本号为1
        AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 1);

        // --- 线程2:模拟 ABA 操作 ---
        new Thread(() -> {
            int stamp = asr.getStamp(); // 获取初始版本号: 1
            System.out.println("线程2:读取初始版本号 " + stamp);
            
            // 第一次修改:A -> B,版本号 1 -> 2
            asr.compareAndSet("A", "B", stamp, stamp + 1);
            
            // 第二次修改:B -> A,版本号 2 -> 3
            asr.compareAndSet("B", "A", asr.getStamp(), asr.getStamp() + 1);
            System.out.println("线程2:完成 ABA 篡改,当前版本号为 " + asr.getStamp());
        }).start();

        // --- 线程1:受害线程尝试更新 ---
        try {
            // 确保线程2先完成 ABA 操作
            Thread.sleep(500); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        int expectedStamp = 1; // 线程1 认为版本号还是 1
        String expectedVal = "A";
        
        // 尝试 CAS:预期值 A,预期版本 1 -> 更新为 C,版本 2
        boolean success = asr.compareAndSet(expectedVal, "C", expectedStamp, expectedStamp + 1);
        
        System.out.println("线程1 CAS 操作结果:" + success); 
        System.out.println("当前实际值:" + asr.getReference() + ",当前实际版本号:" + asr.getStamp());
    }
}

运行结果分析:
虽然当前值确实是 “A”,但由于版本号已经从 1 变为了 3,线程 1 的 CAS 操作会返回 false。ABA 问题被完美解决。


六、 延伸:AtomicMarkableReference

如果不关心变量被修改了几次,只关心它是否被修改过,可以使用更轻量级的 AtomicMarkableReference。它内部维护的是一个 boolean 类型的标记位,逻辑与 AtomicStampedReference 一致,但开销略小。

总结

  1. ABA 问题是 CAS 在检测变量状态时的逻辑漏洞。
  2. 拒绝加锁:应通过引入版本号(Stamp)维持乐观锁的性能优势。
  3. 推荐工具:直接使用 AtomicStampedReference,它通过内部封装 Pair 对象,利用硬件级 CAS 保证了值与版本号更新的原子性。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐