【JUC】什么是ABA问题?

本文探讨了ABA问题在并发编程中的表现及其原因,并通过原子引用和时间戳原子引用(AtomicStampedReference)来解决这一问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

从AtomicInteger引出下面的问题

CAS -> Unsafe -> CAS底层思想 -> ABA -> 原子引用更新 -> 如何规避ABA问题


ABA问题是什么

一句话概括就是:狸猫换太子

假设现在有两个线程,分别是T1 和 T2,然后T1执行某个操作的时间为10秒,T2执行某个时间的操作是2秒,最开始AB两个线程,分别从主内存中获取A值,但是因为B的执行速度更快,他先把A的值改成B,然后在修改成A,然后执行完毕,T1线程在10秒后,执行完毕,判断内存中的值为A,并且和自己预期的值一样,它就认为没有人更改了主内存中的值,就快乐的修改成B,但是实际上 可能中间经历了 ABCDEFA 这个变换,也就是中间的值经历了狸猫换太子。

所以ABA问题就是,在进行获取主内存值的时候,该内存值在我们写入主内存的时候,已经被修改了N次,但是最终又改成原来的值了

CAS导致ABA问题

CAS算法实现了一个重要的前提,需要取出内存中某时刻的数据,并在当下时刻比较并替换,那么这个时间差会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的

ABA问题

CAS只管开头和结尾,也就是头和尾是一样,那就修改成功,中间的这个过程,可能会被人修改过

原子引用

原子引用其实和原子包装类是差不多的概念,就是将一个java类,用原子引用类进行包装起来,那么这个类就具备了原子性

/**
 * 原子引用
 * @author: 陌溪
 * @create: 2020-03-11-22:12
 */

class User {
    String userName;
    int age;

    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "userName='" + userName + '\'' +
                ", age=" + age +
                '}';
    }
}
public class AtomicReferenceDemo {

    public static void main(String[] args) {

        User z3 = new User("z3", 22);

        User l4 = new User("l4", 25);

        // 创建原子引用包装类
        AtomicReference<User> atomicReference = new AtomicReference<>();

        // 现在主物理内存的共享变量,为z3
        atomicReference.set(z3);

        // 比较并交换,如果现在主物理内存的值为z3,那么交换成l4
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());

        // 比较并交换,现在主物理内存的值是l4了,但是预期为z3,因此交换失败
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());
    }
}

基于原子引用的ABA问题

我们首先创建了两个线程,然后T1线程,执行一次ABA的操作,T2线程在一秒后修改主内存的值

/**
 * ABA问题的解决,AtomicStampedReference
 * @author: 陌溪
 * @create: 2020-03-12-15:34
 */
public class ABADemo {

    /**
     * 普通的原子引用包装类
     */
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

    public static void main(String[] args) {

        new Thread(() -> {
            // 把100 改成 101 然后在改成100,也就是ABA
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();

        new Thread(() -> {
            try {
                // 睡眠一秒,保证t1线程,完成了ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 把100 改成 101 然后在改成100,也就是ABA
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());

        }, "t2").start();
    }
}

我们发现,它能够成功的修改,这就是ABA问题

解决ABA问题

新增一种机制,也就是修改版本号,类似于时间戳的概念

T1: 100 1 2019 2

T2: 100 1 101 2 100 3

如果T1修改的时候,版本号为2,落后于现在的版本号3,所以要重新获取最新值,这里就提出了一个使用时间戳版本号,来解决ABA问题的思路

AtomicStampedReference

时间戳原子引用,来这里应用于版本号的更新,也就是每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号

/**
 * ABA问题的解决,AtomicStampedReference
 * @author: 陌溪
 * @create: 2020-03-12-15:34
 */
public class ABADemo {

    /**
     * 普通的原子引用包装类
     */
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

    // 传递两个值,一个是初始值,一个是初始版本号
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {

        System.out.println("============以下是ABA问题的产生==========");

        new Thread(() -> {
            // 把100 改成 101 然后在改成100,也就是ABA
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();

        new Thread(() -> {
            try {
                // 睡眠一秒,保证t1线程,完成了ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 把100 改成 101 然后在改成100,也就是ABA
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());

        }, "t2").start();

        System.out.println("============以下是ABA问题的解决==========");

        new Thread(() -> {

            // 获取版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);

            // 暂停t3一秒钟
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 传入4个值,期望值,更新值,期望版本号,更新版本号
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);

            System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());

            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);

            System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());

        }, "t3").start();

        new Thread(() -> {

            // 获取版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);

            // 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp+1);

            System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:" + atomicStampedReference.getStamp());

            System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());


        }, "t4").start();

    }
}

运行结果为:

我们能够发现,线程t3,在进行ABA操作后,版本号变更成了3,而线程t4在进行操作的时候,就出现操作失败了,因为版本号和当初拿到的不一样

<think>好的,我现在需要详细思考用户的问题,即CAS的概念及其与synchronized的关系。首先,用户可能对并发编程中的基本同步机制感兴趣,所以应该从基础讲起。 CAS,全称Compare-And-Swap,是一种无锁的并发控制机制。我记得CAS操作包含三个操作数:内存位置、预期原值和新值。当执行CAS时,处理器会先比较内存位置的当前值是否与预期原值相等,如果相等则更新为新值,否则不操作。这个过程是原子性的,这很关键,因为它保证了并发环境下的线程安全。 接下来,我需要将CAS与synchronized进行对比。synchronized是Java中的关键字,通过内置锁(monitor)实现同步,属于悲观锁机制。当一个线程进入synchronized块时,它会获取锁,其他线程必须等待。而CAS属于乐观锁,它假设冲突较少,直接尝试更新,如果失败则重试,不需要阻塞线程。 用户可能还想知道两者的优缺点。CAS的优点是无阻塞,减少了线程上下文切换的开销,适合低竞争场景。但在高竞争下,CAS可能导致大量重试,消耗CPU资源。而synchronized在Java 6之后进行了优化,比如偏向锁、轻量级锁等,性能有所提升,但在高竞争时仍然可能引发线程阻塞和上下文切换。 还需要提到Atomic类,比如AtomicInteger,它们底层使用了CAS,这可以举例说明。例如,incrementAndGet()方法就是通过循环CAS实现的。此外,CAS的典型问题,比如ABA问题,以及如何通过版本号解决(如AtomicStampedReference)。 关于应用场景,CAS适合计数器、状态标志等简单操作,而synchronized适合复杂的同步块或需要精细控制锁的情况。另外,JUC包中的并发容器如ConcurrentHashMap使用了CAS和synchronized结合的方式,说明两者可以互补。 最后,确保引用用户提供的参考资料。引用[2]中提到了使用JUC包的并发类,比如ConcurrentHashMap,这可能和CAS的应用有关联。因此,在回答中可以提到这些类的实现可能结合了CAS机制,以提高并发性能。 总结起来,回答的结构应该是先解释CAS的概念,然后与synchronized对比,讨论优缺点,应用场景,以及相关的问题和解决方案,并适当引用提供的资料来支持观点。</think>### Compare-And-Swap (CAS) 的概念 **Compare-And-Swap (CAS)** 是一种无锁(lock-free)的并发编程机制,用于实现线程安全的原子操作。其核心操作包含三个参数: 1. 内存地址 `V` 2. 预期原值 `A` 3. 新值 `B` 操作逻辑为: - 如果内存地址 `V` 的当前值等于 `A`,则将 `V` 的值更新为 `B`,并返回操作成功; - 否则不修改值,并返回操作失败。 这一过程通过硬件支持的原子指令(如 x86 的 `CMPXCHG`)实现,确保操作的原子性。CAS 是乐观锁的典型实现,假设并发冲突较少,通过重试机制而非阻塞线程来处理竞争。 **示例**(Java 中的 `AtomicInteger`): ```java AtomicInteger count = new AtomicInteger(0); count.compareAndSet(0, 1); // 若当前值为0,则更新为1 ``` --- ### CAS 与 synchronized 的对比 | **特性** | **CAS** | **synchronized** | |------------------|---------------------------------------|--------------------------------------| | **锁类型** | 无锁(乐观锁) | 悲观锁 | | **线程阻塞** | 不阻塞,通过自旋重试 | 阻塞,进入等待队列 | | **适用场景** | 低竞争、简单原子操作(如计数器) | 高竞争、复杂同步逻辑 | | **性能开销** | 低(无上下文切换,但高竞争时自旋多) | 高(上下文切换,锁膨胀优化后降低) | | **ABA 问题** | 需额外处理(如版本号) | 无 | --- ### 关键区别与联系 1. **实现方式** - **CAS**:依赖硬件原子指令,直接操作内存值,如 Java 的 `Unsafe` 类或 `Atomic` 包。 - **synchronized**:通过 JVM 内置锁(Monitor)实现,涉及锁升级(偏向锁 → 轻量级锁 → 重量级锁)[^2]。 2. **适用场景** - **CAS**:适合简单变量更新(如计数器、状态标志)。 - **synchronized**:适合复杂临界区(如多步骤操作需原子性)。 3. **ABA 问题** - CAS 可能因变量被其他线程修改后恢复原值而产生逻辑错误,需通过 `AtomicStampedReference` 添加版本号解决。 4. **性能权衡** - **低竞争时**:CAS 性能更优(无阻塞); - **高竞争时**:synchronized 可能更稳定(减少 CPU 空转)。 --- ### 应用示例 - **CAS 使用场景**: ```java // 线程安全的计数器 public class Counter { private AtomicInteger value = new AtomicInteger(0); public void increment() { int oldValue; do { oldValue = value.get(); } while (!value.compareAndSet(oldValue, oldValue + 1)); } } ``` - **synchronized 使用场景**: ```java // 同步方法保证复合操作原子性 public synchronized void transfer(Account from, Account to, int amount) { from.withdraw(amount); to.deposit(amount); } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值