文章目录

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

发布于 2026-04-22 02:23:53 · 浏览 6 次 · 评论 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)

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

扫一扫,手机查看

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