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问题提供了精准的武器。

相关推荐
二月夜12 分钟前
剖析Java正则表达式回溯问题
java·正则表达式
xuhaoyu_cpp_java1 小时前
项目学习(三)分页查询
java·经验分享·笔记·学习
程序员二叉1 小时前
【Java】集合面试全套精讲|HashMap/ArrayList高频考点完整版
java·面试·哈希算法
cfm_29141 小时前
JVM GC垃圾回收初步了解
java·开发语言·jvm
心之伊始2 小时前
LangChain4j RAG 实战:Java 后端如何把本地文档接入 Embedding 检索链路
java·架构·源码分析·csdn
许彰午2 小时前
17_synchronized关键字深度解析
java·开发语言
Xzh04233 小时前
AI Agent 学习路线(Java 后端方向)
java·人工智能·学习
艾利克斯冰4 小时前
Java 设计模式-行为型模式(更新中)
java·开发语言·设计模式
倒霉蛋小马4 小时前
Java新特性:record关键字
java·开发语言
折哥的程序人生 · 物流技术专研5 小时前
《Java 100 天进阶之路》第95篇:消息队列基础(RocketMQ/Kafka)(2026版)
java·面试·kafka·rocketmq·java-rocketmq·求职招聘