CAS与ABA问题

本文介绍了Java中的CAS(Compare And Swap)机制,它用于实现乐观锁,避免了synchronized的锁机制导致的性能问题。然而,CAS存在ABA问题、循环时间长开销大以及只能保证单个共享变量原子操作的问题。为了解决ABA问题,可以使用AtomicStampedReference。同时,文章对比了CAS和synchronized的区别,指出synchronized是悲观锁,而CAS是乐观锁,它们在并发控制上有不同的策略。

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

在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。为了保证原子性操作就引出了CAS

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。

一、什么是CAS

CAS,compare and swap的缩写,翻译成比较并交换(底层的思想用到的乐观锁,也称自旋锁)  

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

//比较并交换 compareAndSet
public class Cas {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);
        //期望更新
        //public final boolean compareAndSet(int expect, int update)
        //如果我们的期望值达到了那我们就更新否则就不更新 CAS是CPU的并发原理
        System.out.println(atomicInteger.compareAndSet(1, 2));
        System.out.println(atomicInteger.get());
        atomicInteger.getAndIncrement();//i++
        System.out.println(atomicInteger.compareAndSet(1, 1));
        System.out.println(atomicInteger.get());
    }
}

 

 Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

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

二、CAS存在的问题

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

1, ABA问题

//比较并交换 compareAndSet
public class Cas {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);
        //ABA问题 线程1
        System.out.println(atomicInteger.compareAndSet(1, 2));
        System.out.println(atomicInteger.get());
        //捣乱的线程 线程2
        System.out.println(atomicInteger.compareAndSet(2, 1));
        System.out.println(atomicInteger.get());
        //线程3
        System.out.println(atomicInteger.compareAndSet(1, 66));
        System.out.println(atomicInteger.get());
        //线程1把1修改为2后线程2又把2修改为1,但是线程3完全不知道1被动过
    }
}

 举一个例子

 怎么解决ABA问题——原子引用

public class CasReference {
    //AtomicReference 注意,如果泛型是一个包装类,注意对象的引用问题
    //参数1 值 参数2 版本号
    static AtomicStampedReference<Integer> atReference = new AtomicStampedReference<>(1,1);

    public static void main(String[] args) {
        new Thread(()->{
            int stamp = atReference.getStamp();//获取版本号
            System.out.println("a1=>"+stamp);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atReference.compareAndSet(1, 2, atReference.getStamp(), atReference.getStamp() + 1));

            System.out.println("a2=>"+atReference.getStamp());

            System.out.println(atReference.compareAndSet(2,3,atReference.getStamp(),atReference.getStamp()+1));

            System.out.println( "a3=>"+atReference.getStamp());
        },"a").start();

        new Thread(()->{
            int stamp = atReference.getStamp();//获取版本号
            System.out.println("b1=>"+stamp);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atReference.compareAndSet(1, 2, stamp, stamp + 1));

            System.out.println("b2=>"+atReference.getStamp());
        },"b").start();
    }
}

线程b就会修改失败,因为版本号已经发生改变

2,循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

3,只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

CAS 与 Synchronized 的对比

  1,synchronized 是悲观的,它假设更新都是可能冲突的,所以要先获取锁,得到锁才更新,它是阻塞式算法,得不到锁就进入锁池等待。

  2,CAS 是乐观的,它假设冲突比较少,但使用CAS 更新,进行冲突检测,如果确实冲突就继续尝试直到成功,它是非阻塞式算法,有更新冲突就重试。

### CAS 中的 ABA 问题及其解决方案 #### 什么是 CASCAS(Compare-And-Swap)是一种用于实现原子操作的技术,广泛应用于多线程环境下的并发编程中。它通过比较内存中的值预期值是否一致来决定是否更新为新值[^3]。 #### ABA 问题的定义 ABA 问题是指在多线程环境下,一个共享变量的值从 A 变为 B,然后又从 B 变回 A,而 CAS 操作可能会错误地认为该值没有被修改过。这种误判可能导致逻辑错误或不正确的程序行为[^4]。 #### ABA 问题的示例代码 以下是一个简单的代码示例,展示了 ABA 问题的发生: ```java import java.util.concurrent.atomic.AtomicReference; public class ABADemo { static AtomicReference<String> ref = new AtomicReference<>("A"); public static void main(String[] args) throws InterruptedException { // 获取初始值 A String prev = ref.get(); // 启动线程 t1 将值从 A 改为 B new Thread(() -> { System.out.println("t1: change A->B " + ref.compareAndSet(ref.get(), "B")); }, "t1").start(); // 线程 t2 将值从 B 改回 A Thread.sleep(500); // 确保 t1 先执行 new Thread(() -> { System.out.println("t2: change B->A " + ref.compareAndSet(ref.get(), "A")); }, "t2").start(); // 主线程尝试将值从 A 改为 C Thread.sleep(1000); // 确保 t1 和 t2 执行完毕 System.out.println("main: change A->C " + ref.compareAndSet(prev, "C")); } } ``` 在上述代码中,主线程期望将值从 A 改为 C,但由于其他线程的操作导致值从 A 到 B 再回到 A,CAS 操作会错误地认为值未被修改,从而允许将值改为 C[^4]。 #### 解决 ABA 问题的方法 为了防止 ABA 问题的发生,可以引入版本号机制,确保每次修改时不仅检查值是否相同,还检查版本号是否匹配。 ##### 使用 `AtomicStampedReference` `AtomicStampedReference` 是 Java 提供的一种解决 ABA 问题的工具类。它通过引入一个额外的“戳记”(stamp)来记录值的变化次数,从而避免单纯的值比较带来的误判。 ###### 示例代码 以下是一个使用 `AtomicStampedReference` 解决 ABA 问题的示例: ```java import java.util.concurrent.atomic.AtomicStampedReference; public class AtomicStampedReferenceDemo { static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); public static void main(String[] args) throws InterruptedException { int[] stampHolder = {ref.getStamp()}; // 启动线程 t1 将值从 A 改为 B,并增加戳记 new Thread(() -> { boolean success = ref.compareAndSet("A", "B", ref.getStamp(), ref.getStamp() + 1); if (success) { System.out.println("t1: change A->B"); } }, "t1").start(); // 启动线程 t2 将值从 B 改回 A,并再次增加戳记 Thread.sleep(500); // 确保 t1 先执行 new Thread(() -> { boolean success = ref.compareAndSet("B", "A", ref.getStamp(), ref.getStamp() + 1); if (success) { System.out.println("t2: change B->A"); } }, "t2").start(); // 主线程尝试将值从 A 改为 C,但需要检查戳记是否匹配 Thread.sleep(1000); // 确保 t1 和 t2 执行完毕 boolean success = ref.compareAndSet("A", "C", stampHolder[0], stampHolder[0] + 1); if (success) { System.out.println("main: change A->C"); } else { System.out.println("main: failed to change A->C due to stamp mismatch"); } } } ``` 在上述代码中,主线程通过检查戳记是否匹配来判断值是否真的未被修改,从而避免了 ABA 问题的发生[^1]。 #### 总结 ABA 问题CAS 操作中常见的一个问题,可能导致程序逻辑错误。通过引入版本号机制,例如使用 `AtomicStampedReference`,可以有效解决这一问题。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值