CAS(CompareandSwap)

CAS

  • [CAS 核心机制](#CAS 核心机制)
  • [CAS 实现的底层逻辑](#CAS 实现的底层逻辑)
  • [CAS 的局限性及解决方案](#CAS 的局限性及解决方案)

CAS 核心机制

CAS(Compare and Swap,比较并交换)是硬件级原生支持的原子操作,是无锁编程的核心基石,也是乐观并发控制的典型实现。其核心价值在于通过硬件原子指令保证单次比较并交换操作的原子性,无需依赖软件层面的锁机制即可实现线程安全的变量更新,从根源上避免了锁竞争带来的线程阻塞与上下文切换开销,是高并发场景下实现轻量级同步的核心技术。CAS 的所有执行逻辑均由 CPU 单条原子指令完成,全程不可中断,这是其得以保证原子性的核心前提;同时其设计遵循乐观并发思想,也是其与传统悲观锁最核心的特征区别。

一、CAS 的核心定义与三要素

CAS(Compare and Swap,比较并交换)是一种针对单个内存地址的硬件级原子更新操作,其操作逻辑始终围绕三个核心参数展开,标准且唯一的表示形式为 CAS (V, E, N)。这三个参数的定义均对应硬件层面语义,是 CAS 操作的核心输入,具体解读如下:

  • V(Memory Location,内存位置):要更新的共享变量对应的唯一内存地址标识,可对应主内存中的物理存储单元,或 CPU 缓存中与主内存保持一致性的缓存行,并非变量值本身。它是 CAS 操作的唯一操作目标,CPU 通过该地址标识,可精准定位到待修改的内存存储单元,确保操作对象的唯一性。
  • E(Expected Value,预期值):线程在执行 CAS 操作前,通过 volatile 语义或硬件原子读取指令,从内存地址 V 中获取的共享变量最新有效值。它是 CAS 操作的核心判断依据,代表当前线程 "认为" 内存地址 V 中应当存储的值,本质是线程对变量当前状态的 "预期假设"------ 假设从读取 E 到执行 CAS 期间,无其他线程修改该变量,且该值未因缓存过期、指令重排序出现偏差。
  • N(New Value,新值):线程根据自身业务逻辑,基于预期值 E 计算得出的希望写入内存地址 V 的目标值。计算过程需保证线程内逻辑连贯,且仅当 CAS 比较判断通过时,才会被 CPU 通过原子指令写入内存地址 V;若比较失败,则该新值会被丢弃,不影响内存中原有值。

二、CAS 的核心执行逻辑

CAS 操作的核心是"比较+交换"的原子性操作,其完整执行流程由 CPU 单条原生原子指令一次性完成,执行期间 CPU 不会响应任何中断、不会切换线程,确保操作全程不可拆分、不可干扰,具体逻辑如下:

  1. 线程通过原子读取指令,从内存地址 V 中获取当前值,作为预期值 E,并基于 E 计算得出新值 N;
  2. CPU 执行 CAS 原子指令,自动读取内存地址 V 中的实际值(记为 V'),并将 V' 与线程持有的预期值 E 进行逐位比对;
  3. 判断逻辑:
    • 当且仅当 V' 与 E 完全相等时,说明从读取 E 到执行 CAS 期间,无其他线程修改过内存地址 V 的值,CPU 立即将新值 N 写入内存地址 V,完成变量更新;
    • 若 V' 与 E 不相等,则说明期间已有其他线程修改过该内存地址的值(或缓存失效导致 V' 与 E 不一致),此时不执行任何更新操作,内存地址 V 中的值仍保持为 V',新值 N 被丢弃。
  4. 执行结果:CAS 操作的执行结果以布尔值返回,用于告知线程操作是否成功,线程可根据该结果执行后续逻辑:
    • 返回 true:表示比较通过、更新成功,内存地址 V 的值已从与 E 相等的 V',原子性更新为新值 N;
    • 返回 false:表示比较失败、未执行任何更新,该内存地址的值仍为 V'(与 E 不相等)。此时线程可选择三种处理方式:① 自旋重试(重新读取 V' 作为新的 E,重新计算 N 后再次执行 CAS);② 降级策略(若自旋次数过多,切换为软件锁机制,避免资源浪费);③ 直接放弃操作(根据业务场景忽略此次更新)。

需要注意的是"比较"与"交换"的不可拆分性是 CAS 保证原子性的核心底层支撑,若两者拆分执行,可能出现比较通过后,未完成交换前,变量被其他线程修改的并发安全问题,而硬件原子指令从根源上杜绝了该问题。

三、CAS 的核心设计思想:乐观并发控制

CAS 的设计完全遵循乐观并发控制(Optimistic Concurrency Control, OCC)的核心思想,这是其设计的底层逻辑,也是其适用场景的核心依据。与 synchronized、ReentrantLock 等基于悲观并发控制(Pessimistic Concurrency Control, PCC)的锁机制相比,两者的核心差异具体对比如下:

  • 乐观并发控制(CAS 实现)

    • 核心假设:默认多线程并发修改变量时的冲突概率极低,无需提前对资源加锁限制线程执行;
    • 同步策略:不阻塞任何线程的执行,线程在修改变量前不获取任何软件锁,直接执行 CAS 操作;若发生冲突(CAS 返回 false),则根据业务需求灵活选择自旋重试、降级为锁或直接放弃,不强制阻塞线程;
    • 线程状态:线程始终处于运行状态(或自旋等待状态),不会因同步问题被操作系统挂起(阻塞),因此无线程上下文切换(保存线程上下文、恢复线程上下文)的开销,性能更优。
  • 悲观并发控制(synchronized、ReentrantLock 等实现)

    • 核心假设:默认多线程并发修改变量时的冲突概率极高,必须提前对资源加锁,才能保证变量更新的线程安全;
    • 同步策略:线程在修改变量前必须先获取锁,若锁已被其他线程持有,则当前线程会被阻塞挂起,进入阻塞队列,直到锁被释放后才能重新竞争锁;
    • 线程状态:线程会在 "运行 - 阻塞 - 就绪" 之间频繁切换,存在明显的上下文切换开销,高并发场景下可能因锁竞争导致性能瓶颈。

CAS 是乐观锁的最底层、最核心实现,也是所有软件层面乐观锁的硬件级技术基石。在我前面这篇博客里已经详细介绍了 MyBatis-Plus的乐观锁与悲观锁,感兴趣可以了解一下。日常开发中常见的数据库乐观锁、分布式乐观锁,以及前文提到的 MyBatis-Plus 乐观锁,其设计本质都离不开 CAS 的 "比较后更新" 核心思想。它们并未改变 CAS "先校验、再更新" 的底层逻辑,只是在 CAS 硬件级原子实现的基础上,增加了版本号、时间戳等软件层面的扩展参数------ 一方面是适配数据库、分布式等不同的业务场景,另一方面也是为了解决 CAS 在纯软件层应用中可能出现的局限性(如 ABA 问题),让乐观锁更贴合实际开发需求,提升并发控制的安全性和实用性。

CAS 实现的底层逻辑

先通过一段可运行代码,验证基于 CAS 实现的 AtomicInteger 的线程安全性,为后续底层解析做铺垫。

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class CasTest {

    //使用AtomicInteger定义a
    static AtomicInteger a = new AtomicInteger();
    public static void main(String[] args) {
        CasTest test = new CasTest();
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        //使用getAndIncrement函数进行自增操作
                        System.out.println(Thread.currentThread().getName() + " 自增后结果:" + a.incrementAndGet());
                        Thread.sleep(500);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
    }
}

这段代码创建了 5 个线程,每个线程循环 10 次执行 AtomicInteger 的原子自增操作,并打印自增后的结果;每次自增后线程休眠 500 毫秒,核心目的是验证多线程环境下 AtomicInteger 自增操作的原子性。

该代码创建 5 个线程,每个线程循环 10 次执行 AtomicInteger 的原子自增操作,每次自增后休眠 500 毫秒以放大线程调度的随机性。最终输出 1~50 的所有数字,数字的打印顺序会因线程调度的随机性而混乱,但无重复、无缺失、无超界。

核心验证:AtomicInteger 的 incrementAndGet() 方法是不可拆分的原子操作,能够有效解决多线程并发修改共享计数器时的线程安全问题。

incrementAndGet() 方法之所以能保证原子性,核心是其底层依赖了 CAS(Compare and Swap)机制。下面我们逐步解析 AtomicInteger 的核心源码,拆解 CAS 实现的完整底层逻辑。

一、 核心成员变量定义

AtomicInteger 中定义了三个核心成员变量,三者协同为 CAS 操作提供必要的前置条件:

java 复制代码
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
  • Unsafe 实例:CAS 操作的底层工具类
    JDK 提供的底层硬件操作工具类,是 Java 代码能够访问底层 CAS 硬件指令的唯一桥梁,后续所有原子操作均依赖该类的 native 方法实现。该类不属于 Java 标准 API,仅对 JDK 内部类开放,普通用户自定义类无法直接通过 Unsafe.getUnsafe() 获取实例。
  • valueOffset:内存偏移量
    类的字段内存偏移量在类加载阶段由 JVM 计算确定,初始化完成后永久不变,不受对象实例化影响;
    后续所有 CAS 操作通过「AtomicInteger 实例对象 + 该内存偏移量」,Unsafe 可以精准定位到该实例中 value 变量对应的内存存储单元,避免操作误触对象的其他字段;
    偏移量的获取依赖 Unsafe.objectFieldOffset() 方法,该方法能提取字段在对象内存布局中的相对偏移地址。
  • value 变量:volatile 修饰的共享计数器
    核心修饰符 volatile 保证多线程间的内存可见性和指令有序性;是 CAS 机制能正常工作的前提,可有效减少自旋 CAS 中的无效重试(避免线程一直基于旧缓存值进行 CAS 比对),大幅提升并发场景下的性能;
    与 CAS 操作配合,共同满足并发编程的 "原子性、可见性、有序性" 三大核心特性。

二、 原子自增方法:incrementAndGet ()

这是调用的自增方法,其核心逻辑是委托给 Unsafe 类的 getAndAddInt() 方法完成,自身仅做简单的返回值处理:

java 复制代码
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
  • final 修饰的意义:
    禁止子类重写该方法,保证原子操作逻辑的唯一性和不可篡改,避免子类修改逻辑导致线程安全问题。
  • 参数说明(对应 Unsafe.getAndAddInt() 入参):
    this:当前 AtomicInteger 实例(CAS 操作的目标对象);
    valueOffset:value 变量的内存偏移量(用于精准定位目标变量);
    1:自增步长(要给 value 增加的数值)。
  • 返回值处理:
    Unsafe.getAndAddInt() 方法返回的是自增前的旧值,因此需要 +1 得到自增后的新值,这是上层 API 为了贴合业务使用习惯做的封装。

三、 核心实现:Unsafe.getAndAddInt(自旋 CAS 逻辑)

getAndAddInt() 是 CAS 机制的核心封装,采用自旋 CAS(循环重试)保证最终更新成功,也是 AtomicInteger 原子性的核心来源,源码如下(基于 JDK 8):

java 复制代码
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

参数映射:

var1:目标对象(此处为 AtomicInteger 实例);

var2:目标变量在对象中的内存偏移量(此处为 valueOffset);

var4:自增步长(此处为 1);

var5:var1对象在地址var2上的期待值,以 volatile 语义获取的value最新值;

var5+var4:基于预期值计算的目标新值(此处为自增 1,var4=1)。

自旋 CAS 核心逻辑:

  • do-while 循环:先执行一次 CAS 操作,失败后进入循环重试,保证至少执行一次 CAS 操作(符合 "先尝试,失败再重试" 的自旋逻辑);
  • getIntVolatile () 方法:以 volatile 语义获取目标对象 var1 中偏移量 var2 对应的变量值赋值给 var5,避免线程读取到 CPU 缓存或工作内存中的旧值,减少无效自旋,提升并发性能;
  • compareAndSwapInt () 方法:
    执行底层硬件级 CAS 原子操作;若var1中偏移量var2对应的变量当前值与预期值var5完全相等(二进制逐位比对),则将其更新为var5 + var4;返回值为 true 表示更新成功,false 表示更新失败(变量已被其他线程修改);
  • 返回旧值:方法返回更新前的旧值var5,而非更新后的新值,这是 Unsafe 类原子方法的统一设计规范,便于上层方法灵活处理(如incrementAndGet()加 1 得到新值)。

三、 底层桥梁:Unsafe 类(Java 与硬件指令的交互入口)

AtomicInteger 的 CAS 操作最终依赖 sun.misc.Unsafe 类实现,该类是 Java 提供的底层硬件操作访问入口,也是 Java 与底层 C/C++、硬件指令之间的桥梁,CAS 机制的核心支撑。

Unsafe 类的核心定位

  • 包路径与方法特性:位于 sun.misc 包下,不属于 Java 标准 API,所有方法均为native方法,无 Java 层面的实现逻辑;
  • 底层交互链路:通过 JNI(Java Native Interface)调用 C/C++ 本地库,最终映射到 CPU 的原生原子指令;
  • 核心功能:提供硬件级别的原子操作、直接内存访问、内存分配 / 释放、线程挂起 / 唤醒等底层功能,是 JDK 并发包(java.util.concurrent)的核心基础;
  • 访问限制:由于功能过于底层,可能直接修改内存地址、破坏 JVM 内存模型,进而影响 JVM 的安全性和稳定性,因此 JDK 对其访问做了* 严格限制,仅允许 JDK 内部类(由启动类加载器 Bootstrap ClassLoader 加载)使用。

CAS 核心方法:compareAndSwapInt(native 方法)

这是实现 int 类型变量 CAS 操作的核心方法,也是AtomicInteger底层依赖的方法,源码如下:

java 复制代码
public final class Unsafe {
    // 修改 int 类型变量的 CAS 操作
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    // 剩余还有很多方法
}
  • 参数介绍:
    第一个参数 var1:CAS 操作的目标对象(对应前文AtomicInteger实例a);
    第二个参数 var2:目标变量在对象中的内存偏移量(有效地址,对应前文valueOffset);
    第三个参数 var3:线程预期的变量当前值(对应前文getAndAddInt中的var5,即 CAS 三要素中的「E(预期值)」);
    第四个参数 var4:线程希望更新的新值(对应前文getAndAddInt中的var5 + var4(步长计算结果),即 CAS 三要素中的「N(新值)」)。
  • native 方法的底层实现:该方法无 Java 层面实现,底层由 HotSpot 虚拟机的 C/C++ 代码实现,映射到 CPU 的原生原子指令,执行过程不可中断,不受操作系统线程调度影响;
  • CPU 原子指令映射:在 x86 架构的 CPU 中,该方法最终映射到 LOCK CMPXCHG 指令,LOCK前缀用于锁定 CPU 缓存行(或系统总线),保证指令执行的原子性;
  • 返回值语义:方法返回布尔值仅表示是否更新成功,不返回变量的最新值,这是 CAS 操作的标准返回结果,便于上层实现自旋重试逻辑;
  • 类型适配:针对 long、Object 不同类型的变量,Unsafe 提供了对应的 CAS 方法(compareAndSwapLong、compareAndSwapObject),核心逻辑一致,仅变量类型不同。

Unsafe 类的核心常用方法分类

JDK的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,它们使用JNI的方式访问本地C++实现库。下面我们来了解一下Unsafe提供的几个主要的方法以及编程时如何使用Unsafe类做一些事情。

  • long objectFieldOffset(Field field) 方法:获取指定字段在所属类中的内存偏移量,用于后续定位字段内存地址;
  • int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址;
  • int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素占用的字节。
  • boolean compareAndSwapLong(Object obj, long offiset, long expect, long update)方法:比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false。
  • public native long getLongvolatile(Object obj, long offset)方法:获取对象obj中偏移量为offset的变量对应volatile语义的值。
  • void putLongvolatile(Object obj, long offset, long value) 方法:设置obj对象中offset偏移的类型为long的field 的值为value,支持volatile语义。
  • void putOrderedLong(Object obj, long offset, long value)方法:设置obj对象中offset偏移地址对应的long型field的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。
  • long getAndSetLong(Object obj, long offset, long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。
java 复制代码
import sun.misc.Unsafe;

public class TestUnSafe {
    //获取Unsafe的实例(2.2.1)
    static final Unsafe unsafe = Unsafe.getUnsafe();
    //记录变量state在类TestUnSafe中的偏移值(2.2.2)
    static final long stateOffset;
    //变量(2.2.3)
    private volatile long state = 0;

    static {
        try {
            //获取state变量在类TestUnSafe中的偏移值(2.2.4)
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
        } catch (Exception ex) {
            System.out.println(ex.getLocalizedMessage());
            throw new Error(ex);
        }
    }

    public static void main(String[] args) {
        //创建实例,并且设置state值为1(2.2.5)
        TestUnSafe test = new TestUnSafe();
        // (2.2.6)
        Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(sucess);

    }
}

在如上代码中,代码(2.2.1) 获取了Unsafe的一个实例,代码(2.2.3) 创建了一个变量state并初始化为0。代码(2.2.4) 使unsafe.objectFieldOffset获取TestUnSafe类里面的state 变量,在TestUnSafe对象里面的内存偏移量地址并将其保存到stateOffset变量中。代码(2.2.6)调用创建的unsafe实例的compareAndSwapInt方法,设置test对象的state变量的值。具体意思是,如果test对象中内存偏移量为stateOffset的state变量的值为0,则更新该值为1。运行上面的代码,我们期望输出true,然而执行后会输出如下结果。

Unsafe 的访问限制与绕过方式

Unsafe.getUnsafe()方法被@CallerSensitive注解标记,JVM 对其做了严格的访问校验,源码如下:

java 复制代码
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

@CallerSensitive 注解是 JVM 特殊处理的注解,用于准确获取方法调用者的类信息,防止通过反射绕过安全检查;

校验逻辑:getUnsafe() 方法会判断调用者的类加载器是否为启动类加载器(Bootstrap ClassLoader),只有 JDK 内部核心类(如 java.util.concurrent 包下的类)才能通过校验;

限制原因:用户自定义类由应用类加载器(Application ClassLoader)加载,会被拒绝访问,避免因滥用 Unsafe 的底层操作破坏 JVM 内存模型和稳定性;

常见报错:用户直接调用 Unsafe.getUnsafe() 会抛出 java.lang.SecurityException: Unsafe 异常。

绕过访问限制的唯一可行方式:反射

通过反射访问 Unsafe 类的静态单例成员 theUnsafe,可以绕过 getUnsafe() 的访问限制,这是自定义使用 Unsafe 的唯一可行方式,自定义实现自旋 CAS 自增,示例代码如下:

java 复制代码
public class TxUnsafe {
    
    static final Unsafe unsafe;
    
    static final long offSet;
    
    volatile  int state = 0;
    
    static{
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            offSet = unsafe.objectFieldOffset(TxUnsafe.class.getDeclaredField("state"));
        } catch (Exception e) {
            e.printStackTrace();
            throw new Error();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TxUnsafe tu = new TxUnsafe();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    increment(tu, offSet);
                }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(tu.state);
    }

    public static void increment(Object obj, long offSet){
        int oldVal = 0;
        do{
             oldVal = unsafe.getIntVolatile(obj, offSet);
        }while (!unsafe.compareAndSwapInt(obj, offSet, oldVal, oldVal+1));
    }
}

核心解析

反射获取 Unsafe:通过反射访问theUnsafe静态成员变量,绕过getUnsafe()的访问限制,这是自定义使用 Unsafe 的唯一可行方式;

自旋 CAS 逻辑:完全模拟AtomicInteger的getAndAddInt()方法,通过do-while循环实现失败重试,保证最终更新成功;

volatile 语义支撑:getIntVolatile()保证每次获取的都是变量最新值,减少无效自旋,提升并发性能;

与 AtomicInteger 同源:自定义方法的核心逻辑与AtomicInteger一致,均依赖 Unsafe 的compareAndSwapInt()方法,验证了 CAS 机制的线程安全性。

注意事项

该方案仅用于学习 CAS 底层原理,生产环境中优先使用 JDK 封装好的Atomic系列类,避免因底层操作失误导致内存泄露、数据错乱等问题;

反射关闭访问权限检查(setAccessible(true))会破坏 Java 的访问控制机制,需谨慎使用;

高并发高冲突场景下,自旋 CAS 会导致大量无效重试,占用 CPU 资源,此时推荐使用ReentrantLock等悲观锁。

四、 核心总结:CAS 实现的底层逻辑全链路

CAS 实现的底层逻辑是一个从上层业务封装到底层硬件执行的完整链路,核心链路可概括为:

AtomicInteger(Java 业务层 API) → Unsafe 类(Java 底层桥梁) → JNI(Java 与 C/C++ 交互) → C/C++ 本地库(HotSpot unsafe.cpp) → CPU 原子指令(如 LOCK CMPXCHG) → 硬件级原子操作

全链路各环节核心作用

上层封装(AtomicInteger):通过自旋 CAS 封装 Unsafe 的底层方法,提供易用、安全的原子操作 API,屏蔽底层复杂的硬件与本地库逻辑,降低开发者使用成本。

核心桥梁(Unsafe 类):通过 native 方法提供 CAS 操作入口,是 Java 代码访问底层硬件指令的唯一途径,承担 "承上启下" 的核心作用。

跨语言交互(JNI):作为 Java 与 C/C++ 本地库的交互接口,将 Unsafe 的 native 方法映射到 C/C++ 实现代码。

本地库实现(C/C++):HotSpot 虚拟机的 unsafe.cpp 等源码,将 JNI 调用转换为具体的 CPU 指令调用逻辑。

硬件支撑(CPU 原子指令):最终由 CPU 执行 LOCK CMPXCHG 等原子指令,通过锁定缓存行 / 系统总线保证操作的原子性,实现不可中断的 "比较并交换"。

关键协同保障:volatile 修饰共享变量保证内存可见性,自旋 CAS 保证软件层面的失败重试,二者与硬件原子指令协同,实现完整的线程安全。

CAS 的局限性及解决方案

ABA 问题是 CAS(Compare and Swap)机制的核心局限性之一,也是乐观并发控制中需要重点规避的潜在风险。

ABA 问题是什么?

当线程 A 读取到共享变量的值为 A(作为 CAS 操作的预期值 E)后,线程 B 先将该变量的值修改为 B,随后又将其修改回 A;当线程 A 后续执行 CAS 操作时,会发现变量当前值仍为 A(与预期值一致),误以为期间没有其他线程修改过该变量,从而成功执行更新操作,但实际上变量已经经历了 A→B→A 的两次修改,这就是 CAS 机制的 ABA 问题。

下面的代码复现了 ABA 问题的核心场景,通过两个线程模拟 A→B→A 的修改过程,验证普通 AtomicInteger 无法感知该变化:

java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ABAAtomic {

    private static AtomicInteger atomicInt = new AtomicInteger(100);

    public static void main(String[] args) throws InterruptedException {
        Thread intT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInt.compareAndSet(100, 101);
                System.out.println("thread intT1:" + atomicInt.get());
                atomicInt.compareAndSet(101, 100);
                System.out.println("thread intT1:" + atomicInt.get());
            }
        });

        Thread intT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean c3 = atomicInt.compareAndSet(100, 101);
                System.out.println("thread intT2:" + atomicInt.get() + ",c3 is:" + c3);        //true
            }
        });

        intT1.start();
        intT2.start();
    }
}

ABA 问题的核心解析

表面结果:线程 T2 的 CAS 操作成功执行,从值的角度看没有任何异常。

本质问题:线程 T2 误以为变量从读取到执行 CAS 期间从未被修改,但实际上变量已经被线程 T1 修改了两次(100→101→100),普通 CAS 仅能完成值的比对,无法感知变量的修改历史。

潜在风险场景:

简单计数器场景(如 AtomicInteger 自增):ABA 问题无实际危害,最终计数器结果仍正确。

带状态的数据结构场景(如链表、栈、队列):ABA 问题可能引发严重错误。例如:链表节点 A 被线程 T1 删除(修改为 B),随后又被线程 T1 重新插入(修改回 A),线程 T2 此时执行节点 A 的 CAS 操作,可能导致链表结构错乱、节点复用异常等问题。

ABA 问题的核心解决方案

JDK 提供了 java.util.concurrent.atomic.AtomicStampedReference 类,专门用于解决 ABA 问题。该类封装了「引用值(Reference)+ 版本戳(Stamp)」双维度比对替代 CAS 单一值比对。

java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAAtomic1 {

    private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);

    public static void main(String[] args) {
        Thread refT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
                System.out.println("thread refT1:" + atomicStampedRef.getReference());
                atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
                System.out.println("thread refT1:" + atomicStampedRef.getReference());
            }
        });

        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                System.out.println("before sleep : stamp = " + stamp);    // stamp = 0
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
                System.out.println("thread refT2:" + atomicStampedRef.getReference() + ",c3 is " + c3);
            }
        });
        refT1.start();
        refT2.start();
    }
}

AtomicStampedReference<Integer>(100, 0) 构造方法有两个参数,第一个参数是引用类型的初始值,第二个参数是 int 类型的初始版本戳。

将变量值与修改标记(版本戳)绑定,打破 CAS 单一值比对的局限,让变量的修改历史可追溯,这是规避 ABA 问题的核心前提。

这段代码首先初始化了一个封装 Integer类型引用值100和初始版本戳0的AtomicStampedReference对象,随后主线程启动两个线程refT1和refT2,其中线程refT2优先执行,先获取当前版本戳0并打印,接着进入2秒休眠;线程refT1启动后先进入1秒休眠(确保refT2已获取初始版本戳),休眠结束后依次执行两次双维度CAS操作,第一次将引用值从100修改为101、版本戳从0自增为1并打印修改后的引用值,第二次再将引用值从101修改回100、版本戳从1自增为2并打印修改后的引用值,完成「A→B→A」的ABA式修改闭环;待refT2完成2秒休眠后,先获取并打印当前已被refT1修改后的版本戳(通常为1或2),随后以初始获取的版本戳0和引用值100为预期,尝试执行CAS操作将引用值改为101、版本戳改为1,最终由于此时引用值虽回到100,但版本戳与预期的0不匹配,导致CAS操作返回false且未执行任何更新,最后打印出引用值仍为100且CAS操作失败的结果,通过「引用值+版本戳」的双维度原子比对,成功规避了CAS机制的ABA问题。

相关推荐
木辰風6 小时前
PLSQL自定义自动替换(AutoReplace)
java·数据库·sql
2501_944525546 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 预算详情页面
android·开发语言·前端·javascript·flutter·ecmascript
heartbeat..6 小时前
Redis 中的锁:核心实现、类型与最佳实践
java·数据库·redis·缓存·并发
7 小时前
java关于内部类
java·开发语言
好好沉淀7 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
gusijin7 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder7 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
lsx2024067 小时前
FastAPI 交互式 API 文档
开发语言
吨~吨~吨~7 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟7 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端