JavaEE中的CAS


CAS

什么是CAS

compare and swap 比较并交换
把内存中的某个值,和CPU寄存器中的某个值,进行比较~
如果两个值相同,就把另一个寄存器中的值和内存的值进行交换(把内存的值放到寄存器B,同时把寄存器B的值写给内存)

CAS伪代码:

下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解CAS 的工作流程

//address 内存的地址, expectValue 寄存器A的值, swapValue 寄存器B的值
boolean CAS(address, expectValue, swapValue) {
    //判断address处放的数据和expectValue是否相同,如果相同,就把寄存器B的值写到内存中
    if (&address == expectedValue) {
        &address = swapValue;
        return true;
    }
    return false;
}

这里的赋值其实是"交换",不关心寄存器B里是啥,更关心内存中是什么,把交换近似的堪称赋值~
上述这一组操作,是通过一个CPU指令完成的!!

上述操作是原子的!!是线程安全的!!同时还高效!!并且还不涉及锁冲突以及线程等待!

  • 基于CAS就可以在实现一些逻辑的时候,不加锁也能保证线程安全

  • CAS是硬件实现的逻辑~

  • CAS看起来比加锁更好,但是CAS只是在特定场景能用,加锁适用面更广,加锁代码往往比CAS可读性更好~

CAS最常用的两个场景:

  1. 实现原子类
  2. 实现自旋锁

在这里插入图片描述

实现原子类

比如多线程count++这个功能,多线程环境下,是线程不安全的,如果想要线程安全,就需要加锁,加锁性能就大打折扣…

这种功能就可以基于CAS操作来实现"原子"的 ++ 操作,从而保证线程安全 & 高效

伪代码实现:

//其实是标准库里已经封装好的一个类
class AtomicInteger {
    private int value;
    //getAndIncrement()方法就相当于count++
    public int getAndIncrement() {
        //此处的oldValue相当于寄存器A,是把内存value的值读到寄存器里
        int oldValue = value;
        //此处的oldValue + 1 也把它理解成是另外一个寄存器B的值
        // 比较看value这个内存中的值,是否和寄存器A的值相同
        //如果相同,就把寄存器B的值,给设置到value中
        //同时CAS返回true,结束循环
        //如果不相同,就继续循环,重新读取内存value的值到寄存器A中
        while ( CAS(value, oldValue, oldValue+1) != true) {
            
            oldValue = value;
        }
        return oldValue;
    }
}

图解:

  1. t1第一次LOAD,把value的值放到t1的寄存器中
    在这里插入图片描述

  2. t1 LOAD完成后,按照顺序,t2 LOAD,把value的值放到t2的寄存器中
    在这里插入图片描述

  3. t2 LOAD完成后,t1进行 CAS,t1中寄存器的值跟value比较,二者值相同,就把另外一个寄存器中0 ++ 的值赋给value
    在这里插入图片描述

  4. t2寄存器的值和内存中的值进行比较,发现不一样,就进入循环,重新LOAD
    在这里插入图片描述

  5. t2 CAS发现数据相同,把另外一个寄存器里准备的 2 放到value
    在这里插入图片描述

在这里插入图片描述

实现自旋锁

自旋锁是纯用户态的轻量级锁在这里插入图片描述

当发现锁被其他线程持有的时候,另外的线程不会挂起等待,而是会反复询问,看当前的锁是否被释放了~

伪代码实现:

public class SpinLock {
    //owner表示当前这把锁是谁获取到的
    //null表示锁是无人获取的状态
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有.
        // 如果这个锁已经被别的线程持有, 那么就自旋等待.
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
        
        //比较owner和null是否相同(是否是解锁的状态),如果是,就要进行交换
        //把当前调用的lock的线程的值,设置到owner里(相当于加锁成功),同时结束循环
        //如果owner不为null,则CAS不进行交换,返回false,会进入循环,又会立即的再次发起判定
        while(!CAS(this.owner, null, Thread.currentThread())){
        }
    }
    public void unlock (){
        this.owner = null;
    }
}

自旋锁这种实现,属于消耗CPU资源,但是换来的是第一时间获取到锁!

这种操作在有些情境下也是值得使用的~
如果当前预期锁竞争不太激烈的时候(预期在短时间内获取到锁),使用自旋锁就非常合适了~

自旋锁是一个轻量级锁,也是一个乐观锁~

在这里插入图片描述

CAS的ABA问题

CAS的一个小缺陷~
举个例子:

我去买个手机,我无法判定这个手机是一个 新机,还是翻新机(二手)
在CAS中,进行比较的时候,发现寄存器A和内存M的值相同
那我无法判定是M始终没变,还是M变了,又变回来了…
一般情况下不会有bug,但是在极端情况下,会出现问题的!

举个ABA在极端情况下的例子:

假设我去超市买零食,零食总价50元,然后扫码付款的时候,卡了下,我就多摁了几下,扫码机器就创建出了两个线程来进行扣款操作,并且扣款是基于CAS来完成:
这是正常的执行方式,没有触发ABS:在这里插入图片描述
但是如果此时妈妈给我的支付宝转账50,我的余额就会重新变为100,此时就会触发ABS:
在这里插入图片描述
这就导致了一次付款,重复扣款的bug!!

💗解决方案:

只要有一个记录,能够记录上 内存 中数据的变化,就可以解决ABA问题了~
💫如何进行记录呢?

另外搞一个内存,保存 M 的 “修改次数” (只增不减)或者是 “上次修改时间”(只增不减),此时修改操作就不是把账户余额读取到 寄存器A 中了,比较的时候也不是比较账户余额了 , 而是比较 版本号/上次修改时间 :
在这里插入图片描述

总结

在这里插入图片描述

你可以叫我哒哒呀
本篇到此结束
“莫愁千里路,自有到来风。”
我们顶峰相见!
  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值