Java中atomic底层原理 - ABA 问题与解决方案

🤔思路分析

  • CAS 存在一个经典问题:

    如果值从 A 变为 B 再变回 A,CAS 会认为没有变化,但实际上变量已被修改过。
    解决方案 :使用带版本戳的原子类

    版本戳解决ABA问题的思路很直接:把一次简单的CAS,变成"值+版本号"的配对CAS。多了一个维度,就多了一层保障。

  • 在 Java 中,AtomicStampedReference 通过一个内部的 Pair 对象将引用值和版本号(stamp)捆绑起来,并基于 volatile 可见性和 CAS 原子性,实现了整个"配对"的原子更新。

🧱 内部结构:配对的核心载体

AtomicStampedReference 的精髓在于其静态内部类 Pair,它充当了原子操作的单元。

java 复制代码
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; // 核心字段,volatile保证可见性
  • Pair 的不可变性referencestamp 都被 final 修饰。这意味着,每次更新状态时,都不是修改旧对象,而是创建一个全新的 Pair 实例来替换旧的。
  • volatile 的可见性pair 变量被 volatile 修饰,这确保了一个线程对 pair 的更新能立即对其他线程可见。

⚙️ 核心操作:compareAndSet 源码解析

compareAndSet 是整个机制的枢纽。它校验当前引用当前版本戳是否都符合预期,只有两者都匹配,才会执行更新。

java 复制代码
public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair; // (1) 获取当前的 Pair 对象

    // (2) 核心校验:引用和版本戳都必须匹配预期值
    return expectedReference == current.reference &&
           expectedStamp == current.stamp &&
           // (3) 优化:如果新值和旧值完全相同,直接返回成功,避免无意义的CAS
           ((newReference == current.reference && newStamp == current.stamp) ||
            // (4) 核心CAS操作:尝试用新Pair对象替换旧Pair对象
            casPair(current, Pair.of(newReference, newStamp)));
}

// 底层CAS实现,依赖Unsafe类
private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
  • 逻辑拆解
    1. 获取当前快照Pair<V> current = pair; 获取当前最新的 Pair 对象。
    2. 双重校验 :必须同时满足 expectedReference == current.reference(引用匹配)和 expectedStamp == current.stamp(版本戳匹配),CAS才能继续。
    3. 无变化优化 :如果发现 newReferencenewStamp 与当前值一模一样,则直接返回 true,这是一种避免无意义CAS的自优化手段。
    4. CAS替换 :通过 casPair 方法执行底层的 compareAndSwapObject,尝试将 pair 引用指向新创建的 Pair.of(newReference, newStamp) 对象。这一步是原子的,是最终保障。

✅ 实战对比:AtomicStampedReference 如何精准拦截 ABA

下面通过一个代码示例,直观地对比 AtomicReference(无版本戳)和 AtomicStampedReference(有版本戳)在处理ABA问题时的不同表现。

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

public class ABADemo {

    public static void main(String[] args) throws InterruptedException {
        // 模拟没有版本戳的ABA问题
        AtomicReference<Integer> ref = new AtomicReference<>(1);
        Thread t1 = new Thread(() -> {
            Integer value = ref.get(); // value = 1
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            // 尽管中间被改成2又改回1,但由于值还是1,CAS成功!
            System.out.println("AtomicReference CAS result: " + ref.compareAndSet(value, 3));
        });
        Thread t2 = new Thread(() -> {
            ref.set(2);
            ref.set(1);
        });
        t1.start(); t2.start();
        t1.join(); t2.join(); // 输出: AtomicReference CAS result: true

        // 模拟使用AtomicStampedReference解决ABA问题
        AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(1, 0);
        Thread t3 = new Thread(() -> {
            int[] stampHolder = new int[1];
            Integer value = stampedRef.get(stampHolder); // 获取当前值和版本戳
            int stamp = stampHolder[0];
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            // 尽管值还是1,但版本戳0已不匹配(现在是2),所以CAS失败!
            System.out.println("AtomicStampedReference CAS result: " +
                stampedRef.compareAndSet(value, 3, stamp, stamp + 1));
        });
        Thread t4 = new Thread(() -> {
            // 执行ABA操作,每次修改都递增版本戳
            int[] stampHolder = new int[1];
            int value = stampedRef.get(stampHolder);
            int stamp = stampHolder[0];
            stampedRef.compareAndSet(value, 2, stamp, stamp + 1);
            value = stampedRef.get(stampHolder);
            stamp = stampHolder[0];
            stampedRef.compareAndSet(value, 1, stamp, stamp + 1);
        });
        t3.start(); t4.start();
        t3.join(); t4.join(); // 输出: AtomicStampedReference CAS result: false
    }
}

🆚 补充:与 AtomicMarkableReference 的区别

JDK 还提供了 AtomicMarkableReference,它也使用 Pair,但 stamp 被替换成了 boolean mark,只记录对象是否被修改过,不关心修改次数。

特性 AtomicStampedReference AtomicMarkableReference
版本戳类型 int,可记录修改次数 boolean,只记录是否被修改
适用场景 需要精确知晓变量被修改了多少次 只关心变量是否曾被修改过
示例 账户余额变动次数 订单是否已被处理

⚠️ 注意事项

  • 动态获取版本戳 :强烈推荐使用 get(int[] stampHolder) 方法来一次性获取当前的引用值和版本戳,这避免了分别调用 getReference()getStamp() 带来的时间差问题。
  • 版本戳选择 :通常使用一个单调递增的整数(例如stamp+1)作为新版本戳即可满足大多数业务需求,这能充分保证每个版本的唯一性。

可以看到,AtomicStampedReference 通过引入版本戳,在硬件层面实现了一套可靠的"版本控制"方案,为解决ABA问题提供了精准的武器。

相关推荐
无关86885 小时前
Spring Boot 项目标准化部署打包实战
java·spring boot·后端
jay神5 小时前
基于微信小程序课外创新实践学分认定系统
java·spring boot·小程序·vue·毕业设计
Gauss松鼠会5 小时前
GaussDB(DWS) GUC参数修改、查看
java·数据库·sql·数据库开发·gaussdb
AIFQuant6 小时前
Java 对接全球股票实时报价:高可用架构与异常处理
java·开发语言·websocket·金融·架构·股票api
未若君雅裁6 小时前
Spring Bean 作用域、线程安全与生命周期
java·安全·spring
奋斗的小乌龟6 小时前
langchain4j笔记-智能体系统01
java·笔记
wh_xia_jun6 小时前
用pom 的test 配置 与 jacoco
java·ide·intellij-idea
阿丰资源6 小时前
基于Spring Boot的酒店客房管理系统
java·spring boot·后端
无籽西瓜a6 小时前
【西瓜带你学Kafka | 第八期】 Kafka的主从同步、消息可靠性、流处理与顺序消费(文含图解)
java·分布式·后端·kafka·消息队列·mq