【Java并发】CAS 机制详解和 ABA 问题的解决

目录

什么是 CAS

CAS 的工作原理

三个关键参数

CAS 操作基本步骤

CAS 的核心 UnSafe

CAS 的使用

CAS 优缺点

优点

缺点

什么是 ABA 问题

ABA 问题的解决


什么是 CAS

CAS(Compare-And-Swap)是一种用于在多线程编程中实现原子操作的技术,是硬件级别的原子操作,它比较内存中的某个值是否为预期值,如果是,则更新为新值,否则不做修改。

CAS 的工作原理

三个关键参数

  • 内存位置(V):需要检查和更新的变量的内存地址。

  • 预期原值(A):线程预期的变量当前值。

  • 新值(B):如果变量的当前值与预期原值相匹配,那么将变量更新为这个新值。

CAS 操作基本步骤

  1. 比较:首先,线程会检查内存位置的值是否与预期原值相等。

  2. 交换:如果值相等,线程会将内存位置的值更新为新值。

  3. 返回:操作完成后,CAS会返回一个布尔值,表示操作是否成功。

如果内存位置的值在比较和交换之间被其他线程更改,那么CAS操作会失败,因为内存位置的值与预期原值不再匹配。

CAS 的核心 UnSafe

UnSafeCAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问, UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据。

Unsafe类中的所有方法都是Native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。

CAS 的使用

下面是一个简单案例:

public class CASDemo {
    public static void main(String[] args) {
        // 创建一个 AtomicInteger 对象,并初始化为 5
        AtomicInteger atomicInteger = new AtomicInteger(5);

        // 使用 compareAndSet 方法尝试将 atomicInteger 的值从 5 改为 2020
        // 如果当前值是 5,则修改成功,返回 true,并且 atomicInteger 的值变为 2020
        // 否则,修改失败,返回 false,atomicInteger 的值保持不变
        // 期望的结果是 true,因为当前值确实是 5
        System.out.println(atomicInteger.compareAndSet(5, 2020) + "=>" + atomicInteger.get());

        // 再次使用 compareAndSet 方法尝试将 atomicInteger 的值从 5 改为 1024
        // 由于上一步操作已经将 atomicInteger 的值改为 2020,这次比较会失败
        // 因此,返回 false,atomicInteger 的值保持为 2020
        // 期望的结果是 false,因为当前值不再是 5
        System.out.println(atomicInteger.compareAndSet(5, 1024) + "=>" + atomicInteger.get());
    }
}
  • 第一次compareAndSet调用成功,因为atomicInteger的值确实是5,所以值被更新为2020。

  • 第二次compareAndSet调用失败,因为atomicInteger的值已经是2020,与预期值5不匹配,所以值保持不变,仍然是2020。

CAS 优缺点

优点

  • 无锁并发:CAS操作不使用锁,因此不会导致线程阻塞,提高了系统的并发性和性能。

  • 原子性:CAS操作是原子的,保证了线程安全。

缺点

  • ABA问题:CAS操作中,如果一个变量值从A变成B,又变回A,CAS无法检测到这种变化,可能导致错误。

  • 自旋开销:CAS操作通常通过自旋实现,可能导致CPU资源浪费,尤其在高并发情况下。

  • 单变量限制:CAS操作仅适用于单个变量的更新,不适用于涉及多个变量的复杂操作。

下面我们来详细讲一讲 ABA 问题。 

什么是 ABA 问题

ABA 问题是指在并发编程中,当一个变量的值从A变为B,再从B变回A时,如果使用 CAS 操作,该操作可能会错误地认为变量的值没有改变,从而更新变量为另一个值,这可能导致数据不一致的问题。

ABA 问题示例:

import java.util.concurrent.atomic.AtomicInteger;

public class ABADemo {
    private static AtomicInteger value = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            value.set(1); // 线程1将值设置为1
            value.set(0); // 线程1将值改回0
        });

        Thread thread2 = new Thread(() -> {
            int originalValue = value.get(); // 读取值为0
            while (originalValue == 0) {
                if (value.compareAndSet(originalValue, 2)) { // 尝试将0改为2
                    System.out.println("Value changed to 2");
                    break;
                }
                originalValue = value.get(); // 重新读取值
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }
}

本示例中,thread1value从0改为1,然后又改回0。thread2尝试将value从0改为2。由于thread1的操作,thread2可能会错误地认为value仍然是0,并成功地将其改为2,即使value实际上已经经历了变化。

ABA 问题的解决

解决ABA问题的常见方法是使用带有版本号的原子引用类,如AtomicStampedReference。这个类通过维护一个“版本号”或“时间戳”来确保操作的原子性和正确性,类似乐观锁。

使用AtomicStampedReference解决ABA问题的代码示例:

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABASolutionDemo {
    private static AtomicStampedReference<Integer> value = new AtomicStampedReference<>(0, 0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            int[] stamp = new int[1];
            value.getReference(stamp); // 获取当前值和版本号
            value.set(1, stamp[0] + 1); // 线程1将值设置为1,并增加版本号
            value.set(0, stamp[0] + 1); // 线程1将值改回0,并增加版本号
        });

        Thread thread2 = new Thread(() -> {
            int[] stamp = new int[1];
            while (value.getReference(stamp) == 0) {
                if (value.compareAndSet(0, 2, stamp[0], stamp[0] + 1)) { // 尝试将0改为2,并增加版本号
                    System.out.println("Value changed to 2");
                    break;
                }
                stamp[0] = value.getStamp(); // 重新获取版本号
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }
}

在这个解决方案中,AtomicStampedReference维护了一个值和一个版本号。每次更新值时,版本号都会增加,这样即使值被改回原来的值,版本号也已经改变,从而避免了ABA问题。compareAndSet方法现在需要检查值和版本号是否都匹配,只有两者都匹配时才会更新值和版本号。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hrhcode

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值