并发编程(六)CAS下ABA问题及解决

系列文章目录

一:计算机模型&volatile关键字详解
二:java中的锁体系
三:synchronized关键字详解
五:Atomic原子类与Unsafe魔法类详解
六:CAS下ABA问题及解决



一、CAS问题引入

在并发问题中,最先想到的无疑是互斥同步,但线程阻塞和唤醒带来了很大的性能问题,同步锁的核心无非是防止共享变量并发修改带来的问题,但不是任何时候都有这样的竞争关系。

二、CAS是什么

CAS,比较并交换(Compare-and-Swap,CAS),如果期望值和主内存值一样,则交换要更新的值,也称乐观锁,是一种常见的降低读写锁冲突,保证数据一致性的乐观锁机制。

如线程甲从主内存中拷贝了变量A为1,在自己的线程中将副本A改为了10,当线程甲准备把这个变量更新到主内存时,如果主内存A的值不改变(期望值),还是1,那么线程甲成功更新主内存中A的值。但如果主内存A的值已经先被其他线程改掉不为1,那么线程甲不断地重试,直到成功为止(自旋)
CAS 的思想很简单:三个参数,一个当前内存值 V、旧的预期值 A、即将更新的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false

三、ABA问题分析

1、问题的引入

  • 银行存取钱

在这里插入图片描述

  • 小偷偷钱

在这里插入图片描述
在这里插入图片描述

2、ABA问题代码

 static AtomicInteger atomicInteger = new AtomicInteger(1);
    public static void main(String[] args) {
       CompletableFuture.runAsync(()->{
           int a = atomicInteger.get();
           System.out.println("操作线程"+Thread.currentThread().getName()+"--修改前操作数值:"+a);
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           boolean isCasSuccess = atomicInteger.compareAndSet(a,2);
           if(isCasSuccess){
               System.out.println("操作线程"+Thread.currentThread().getName()+"--Cas修改后操作数值:"+atomicInteger.get());
           }else{
               System.out.println("CAS修改失败");
           }
       });

        CompletableFuture.runAsync(()->{
            atomicInteger.incrementAndGet();// 1+1 = 2;
            System.out.println("操作线程"+Thread.currentThread().getName()+"--increase后值:"+atomicInteger.get());
            atomicInteger.decrementAndGet();// atomic-1 = 2-1;
            System.out.println("操作线程"+Thread.currentThread().getName()+"--decrease后值:"+atomicInteger.get());
        });

    }

执行结果:线程2获取到值等于1,此时线程3得到的值也是1,此时+1等于2,然后再减1等于1,此时线程1 更新操作,期望值是1,更新值是2,所以能更新成功,但是在这期间已经存在线程3对此值进行了两次操作。
在这里插入图片描述

3、并发业务场景ABA导致的问题已经解决

ABA问题导致的原因,是CAS过程中只简单进行了“值”的校验,在有些情况下,“值”相同不会引入错误的业务逻辑(例如订单库存),有些情况下,“值”虽然相同,却已经不是原来的数据了

3.1、问题

系统中,某个商品的总库存为5,用户1和用户2同时下单购买
用户1购买了3个库存,于是库存要设置为2
用户2购买了2个库存,于是库存要设置为3
这两个设置库存的接口并发执行,库存会先变成2,再变成3,导致数据不一致(实际卖出了5件商品,但库存只扣减了2,最后一次设置库存会覆盖和掩盖前一次并发操作)

3.2、原因

出现数据不一致的根本原因,是设置操作发生的时候,没有检查库存与查询出来的库存有没有变化,理论上:
仅库存为5的时候,用户1的库存设置2才能成功
仅库存为5的时候,用户2的库存设置3才能成功

实际执行的时候:
库存为5,用户1的set stock 2确实应该成功

库存变为2了,用户2的set stock 3应该失败掉

3.3、解决

在上面例子中,执行更新操作时,带上版本号即可

四、ABA问题解决

优化方向:CAS不能只比对“值”,还必须确保的是原来的数据,才能修改成功。

1、AtomicStampReference

AtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:

//参数代表的含义分别是 期望值,写入的新值,期望标记,新标记值
public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp);
public V getRerference();
public int getStamp();
public void set(V newReference,int newStamp);

2、AtomicStampReference的使用实例

    private static AtomicStampedReference<Integer> atomicStampedRef =
            new AtomicStampedReference<>(1, 0);
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        CompletableFuture.runAsync(()->{
            int stamp = atomicStampedRef.getStamp(); //获取当前标识别
            System.out.println("操作线程" + Thread.currentThread()+ "stamp="+stamp + ",初始值 a = " + atomicStampedRef.getReference());
            try {
                Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
            System.out.println("操作线程" + Thread.currentThread() + "stamp="+stamp + ",CAS操作结果: " + isCASSuccess);
            countDownLatch.countDown();
        });
        CompletableFuture.runAsync(()->{
            int stamp = atomicStampedRef.getStamp();
            atomicStampedRef.compareAndSet(1,2,stamp,stamp+1);
            System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【increment】 ,值 = "+ atomicStampedRef.getReference());
            stamp = atomicStampedRef.getStamp();
            atomicStampedRef.compareAndSet(2,1,stamp,stamp+1);
            System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【decrement】 ,值 = "+ atomicStampedRef.getReference());
            countDownLatch.countDown();
        });
        countDownLatch.await();
    }

执行结果:线程2获取到值等于1,标记值也是0,此时线程3得到的值也是1,标记值也是0,此时+1等于2,标记值+1是1;然后再减1等于1,标记值+1等于2,此时线程1 更新操作,期望值是1,标记值是0,更新值是2,所以不能更新成功
在这里插入图片描述

总结

其实除了AtomicStampedReference类,还有一个原子类也可以解决,就是AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,用法没有AtomicStampedReference灵活。因此也只是在特定的场景下使用

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值