文章目录

Java AtomicStampedReference解决ABA问题的版本号机制

发布于 2026-04-22 02:23:53 · 浏览 161 次 · 评论 0 条

Java AtomicStampedReference解决ABA问题的版本号机制

在多线程环境中使用 CAS(Compare-And-Swap)操作时,会遇到一个被称为“ABA 问题”的典型陷阱。简单来说,就是一个共享变量的值从 A 变成了 B,又从 B 变回了 A。其他线程若只检查值,会误以为它从未被修改过,从而引发不可预期的错误。AtomicStampedReference 类通过引入“版本号”机制,完美解决了这一问题。


理解 ABA 问题的本质

想象这样一个场景:你桌上有一杯水(值 A),你离开了一会儿。期间,同事把水喝了(值变为空/B),然后又重新倒满了一杯新水(值又变回 A)。当你回来时,看到杯子里的水还是满的,以为这杯水一直没动过,拿起来就喝。实际上,这杯水已经被替换过了。

在编程中,这种“值未变但状态已变”的情况会导致严重的数据一致性问题,例如在链表操作中导致节点丢失。

解决方案核心:版本号机制

AtomicStampedReference 的核心思想是在维护对象引用的同时,维护一个整数类型的“版本号”(通常称为 Stamp)。

  • 传统 CAS:只检查内存值 V 是否等于预期值 A。
  • 带版本号的 CAS:检查内存值 V 是否等于预期值 A,并且检查当前版本号是否等于预期版本号。

只要对象被修改过,哪怕值变回了原样,版本号也必须递增。这样,CAS 操作就能通过对比版本号识别出值已经被改动过。


实操步骤与代码演示

下面通过对比 AtomicInteger(存在 ABA 问题)和 AtomicStampedReference(解决 ABA 问题),演示如何正确使用版本号机制。

1. 模拟 ABA 问题环境

我们需要两个线程:一个线程负责制造 ABA 变化,另一个线程负责尝试更新。

2. 编写对比代码

创建一个名为 ABADemo 的类,并在其中定义两个原子变量:一个普通的 AtomicInteger 和一个带版本号的 AtomicStampedReference。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {
    // 普通的原子类,存在 ABA 问题
    private static AtomicInteger atomicInt = new AtomicInteger(100);

    // 带版本号的原子引用,初始值为 100,初始版本号为 0
    private static AtomicStampedReference<Integer> atomicStampedRef = 
        new AtomicStampedReference<>(100, 0);

    public static void main(String[] args) throws InterruptedException {
        // --- 测试 AtomicInteger 的 ABA 问题 ---
        System.out.println("--- 测试 AtomicInteger ---");

        // **创建**线程 t1,模拟 ABA 过程:100 -> 101 -> 100
        Thread t1 = new Thread(() -> {
            atomicInt.compareAndSet(100, 101);
            atomicInt.compareAndSet(101, 100);
        });

        // **创建**线程 t2,尝试将 100 更新为 101
        Thread t2 = new Thread(() -> {
            try {
                // **暂停** 1 秒,确保 t1 执行完 ABA 操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // **执行** CAS 操作
            boolean isSuccess = atomicInt.compareAndSet(100, 101);
            System.out.println("AtomicInteger CAS 结果: " + isSuccess); // 预期输出 true,但这其实是错误的成功
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        // --- 测试 AtomicStampedReference 解决 ABA ---
        System.out.println("\n--- 测试 AtomicStampedReference ---");

        // **创建**线程 refT1,模拟带版本号的 ABA 过程
        Thread refT1 = new Thread(() -> {
            try {
                // **暂停** 1 秒,让主线程先拿到初始版本号
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // **获取**当前版本号
            int stamp = atomicStampedRef.getStamp();
            System.out.println("refT1 操作前版本号: " + stamp);

            // **执行**更新:100 -> 101,版本号 +1
            atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
            // **执行**更新:101 -> 100,版本号再 +1
            stamp = atomicStampedRef.getStamp(); // 获取新的版本号
            atomicStampedRef.compareAndSet(101, 100, stamp, stamp + 1);
            System.out.println("refT1 操作后版本号: " + atomicStampedRef.getStamp());
        });

        // **创建**线程 refT2,尝试利用旧版本号进行更新
        Thread refT2 = new Thread(() -> {
            // **获取**初始值和初始版本号(此时版本号还未被 refT1 修改)
            int oldStamp = atomicStampedRef.getStamp();
            System.out.println("refT2 拿到的初始版本号: " + oldStamp);
            try {
                // **暂停** 2 秒,确保 refT1 已经完成了 100->101->100 的过程
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // **尝试**更新:期望值 100,新值 101,期望版本号 oldStamp,新版本号 oldStamp + 1
            boolean isSuccess = atomicStampedRef.compareAndSet(100, 101, oldStamp, oldStamp + 1);
            System.out.println("AtomicStampedReference CAS 结果: " + isSuccess); // 预期输出 false
            System.out.println("当前实际版本号: " + atomicStampedRef.getStamp());
        });

        refT1.start();
        refT2.start();
    }
}

3. 运行结果分析

执行上述代码,观察控制台输出。

对于 AtomicInteger,CAS 操作返回 true。这表明尽管值中间被修改过,但 CAS 依然认为没有变化,这也就是 ABA 问题的隐患所在。

对于 AtomicStampedReference,CAS 操作返回 false。

  1. 线程 refT2 最初获取到版本号为 0。
  2. 线程 refT1 将值从 100 改为 101(版本号变为 1),又改回 100(版本号变为 2)。
  3. 线程 refT2 醒来后尝试更新,它提供的期望版本号是 0,但内存中实际的版本号已经是 2。
  4. 因为版本号不匹配,更新被拒绝,从而保证了数据的安全性。

常用方法解析

在使用 AtomicStampedReference 时,主要涉及以下方法:

  • get():返回当前引用值及其对应的版本号。通常需要传入一个 int 数组来接收版本号。
  • getReference():仅返回当前的引用对象。
  • getStamp():仅返回当前的版本号(整数)。
  • compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp):核心方法。如果当前引用等于 expectedReference 且当前版本号等于 expectedStamp,则以原子方式将引用更新为 newReference,将版本号更新为 newStamp。
  • attemptStamp(V expectedReference, int newStamp):如果当前引用持有预期的引用值,则以原子方式将版本号设置为 newStamp。

通过在每一次修改数据时强制更新版本号,AtomicStampedReference 能够准确识别出“值虽然回到了原点,但中间发生过变化”的情况,是解决并发编程中 ABA 问题的标准方案。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文