【多线程】CAS的应用 | CAS的概念 | 实现原子类 | 实现自旋锁 | ABA问题


一、CAS

1.什么是CAS

Compare and swap 比较并交换。

比较交换的是 内存 和 寄存器

比如此时有一个内存 : M。 还有两个寄存器A,B

​ CAS ( M , A , B ) :如果M和A的值相同的话,就把M和B的值进行交换(交换的目的是为了把B赋值给M,M = B),同时整个操作返回 true。如果M和A的值不同的话,无事发生,同时整个操作返回false。

//伪代码:
boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}
  • CAS其实是一个CPU指令,一个CPU指令,就可以完成上述比较交换的逻辑。
  • 单个的CPU指令是原子的。所以可以使用CAS来完成一些操作来代替“加锁”。为编写线程安全的代码提供了线的思路,基于CAS实现线程安全的方式,也叫“无锁编程”。

优点:保证线程安全,同时避免发生阻塞。

缺点:代码可读性差,并且只能适用在特定场景,不如加锁普适。

​ CAS本质上是CPU提供的指令,被操作系统封装后,提供成为APi。又再次被JVM进行封装,提供成API,最终由程序员调用方法实现操作。

2.实现原子类

之前int count++的操作,就不是原子的(load,add,save)

在Java标准库中,提供了原子类,例如AtomicInteger,基于CAS的方式对int进行封装,++就封装成原子操作了。

    public static AtomicInteger count = new AtomicInteger(0);
    //对int进行原子化包装
    //0作为构造方法的参数
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
               count.getAndIncrement();//count++; 后置++
//                count.incrementAndGet();//++count  前置++
//                count.getAndDecrement();//count--; 后置--;
//                count.decrementAndGet();//--count 前置--
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

在Java中,有些操作是偏底层的操作,在使用时有着更多的注意事项,稍有不慎就容易写出问题。这些操作就同一放在unsafe这个类中

在这里插入图片描述

//伪代码
class AtomicInteger {
    private int value;//相当于内存
    public int getAndIncrement() {
        int oldValue = value;
        //oldValue相当于寄存器    
        while ( CAS(value, oldValue, oldValue+1) != true) {
                  //CAS(内存,寄存器A,寄存器B)
            oldValue = value;
       }
        return oldValue;
   }
}

寄存器A先保存当前内存的值,CAS来判断此时有没有被穿插执行。如果没有穿插执行,内存的值等于寄存器A的值,把寄存器B(B执行+1操作)的值赋值给内存,同时CAS返回的是true,循环不成立,返回是是自增前的值。

如果被穿插执行了,CAS中,内存的值不等于寄存器A的值,CAS直接返回false.此时循环成立,将被穿插执行过的内存值,重新赋值给寄存器A,也就是说要再读一遍内存。再次执行CAS。

线程不安全的本质是进行操作的过程中,穿插执行。

1.加锁是通过阻塞的方式,避免穿插。

2.CAS是通过重试的方式,避免穿插。如果值和之前不相等了,重新更新一下,在进行操作。

3.实现自旋锁
//伪代码:
public class SpinLock {
    private Thread owner = null;//记录当前这个锁被哪个线程获取到。 null表示未加锁
    public void lock(){//加锁
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){//解锁
        this.owner = null;
   }
}
  • while循环中,CAS判断当前owner是不是未加锁状态。如果是未加锁的,就把owner赋值成 Thread.currentThread())(调用lock方法的线程引用)进行加锁。如果已经被加锁了,CAS返回false,取反之后,while就会反复执行。一旦其他线程释放了这个锁,owner变成null时,因为while不停循环,就会立即拿到当前这把锁。

  • 缺点:while循环在忙等的过程中,不断消耗CPU资源。如果加锁的概率不大,付出的CPU资源就是值得的。如果当前的锁冲突很激烈,可能会拿不到,此时自旋锁就不适用了。

4.CAS的ABA问题

​ CAS进行操作的关键,是通过值“没有发生变化”来作为 “没有其他线程穿插执行” 的判定依据。但是这种判定方式并不严谨,比如,可能会有别的线程穿插进来,把A改为B后,再改回A。针对第一个线程来说,看起来A的值没变,但是实际上已经被穿插执行了。

比如二手的翻新机,当做新机来买。已经被拆开来,又拼成原来的样子。普通人无法进行分别。

  • 正常情况下,即使出现了ABA问题,逻辑上也不一定会出现bug。但是在极端情况下存在bug

​ 比如,在取钱的时候,账户余额有1000块钱,要取500。按第一次按钮的时候,由于没反应,就又、按了一次按钮。此时相当于创建了两个线程来进行取钱操作。假设线程1读取了1000的余额后,被线程2穿插执行。线程2取了500(此时内存被赋值成500,寄存器A则为1000).轮到线程1时,CAS判断内存中的500和寄存器A中的1000不相等,就不会执行操作。此时扣款就是真确的,只扣了一次。

​ 但是如果在线程2执行完了之后,有人(线程3)往卡里打款了500.此时,内存的值又变成了1000.形成了ABA问题。当轮到线程1执行的时候。CAS判断内存中的1000和寄存器中的1000是相等的,会再次扣除500。预期是取500,实际上取了1000出来。

解决方案:
  • 只要让判定的数值,让一个方向增长。(有增有减才有可能产生ABA问题,只是增加,或者只是减少就没有bug)

针对余额这种能增能减的变量,可以引入“版本号”的概念。约定每次修改余额,都让版本号自增。

在使用CAS判定的时候,判定是是版本号而不再余额。如果版本号不变,就没有被其他线程穿插执行。

点击移步博客主页,欢迎光临~

偷cyk的图

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值