Java多程序 CAS的原理和实现

我们知道要解决一个并发问题,可以给代码加synchronized关键字,这样就能保证数据保持同步,但synchronized存在一个问题,就是每次都会加锁,synchronized是使用操作系统底层的互斥锁(Mutex Lock)实现的,非常的消耗资源,这样就导致程序性能减低。我们称这种锁为悲观锁,悲观锁非常悲观也非常严格,认为并发的情况非常严重并且非常频繁,每次操作都要检查。但同时,JVM还提供了另外一种锁机制,这种锁机制认为,并发出现的概率并没有那么高,所以干脆不加锁,而是在出现并发的时候,数据回滚,并让线程重试,直到没有出现并发为止。这也是一种锁,锁的关键就是回滚和重试,我们称这种锁为乐观锁
JVM乐观锁的实现就是一种叫做CAS(Compare And Swap,比较并交换)的机制。

实际上CAS和synchronized都是JVM提供的功能。我们不需要关心底层具体是怎么实现的,但需要明白原理。他们都是CPU原语。

CAS和synchronized的区别不仅仅是所谓的悲观乐观。主要区别还是用途的区别。

  1. 如果程序的并发性不高,可以考虑用CAS。例如多个窗口买票的例子,10000张票可能出现十次并发。但这只能算低并发。如果要举高并发的例子,可能就是定时抢票的例子,10000张票10秒钟抢完。这时候10000张票可能出现1000次并发。那么就要考虑用synchronized了。
  2. CAS只能用于变量,如果存在多条语句的情况,还是只能用synchronized。

在JUC中的有一系列原子类AtomicBoolean,AtomicInteger,AtomicDouble对应Java中的boolean,int和double。这些类的底层都是通过CAS实现的。通过这些类我们可以将下面的用synchronized同步num++的代码替换为CAS的实现方式。也是能够解决并发问题的,并且在低并发的前提下,效率更高。

synchronized (this){
   num++;
}

AtomicInteger底层是基于CAS实现的。

AtomicInteger num=new AtomicInteger(10);
num.incrementAndGet();

CAS并发原语体现在java语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

CAS是一条CPU并发原语;它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

我们看一个例子:

1.在内存地址V当中,存储着值为10的变量。对这个值进行累加操作。内存地址对于多个线程是共享的。下面都用绿色表示。

在这里插入图片描述

2.此时线程一想把变量的值增加1.对线程一来说,旧的预期值A=10,要修改的新值B=11.此时V的值为10。
在这里插入图片描述
3. 在线程一要提交更新之前,另一个线程二抢先一步,把内存地址V中的变量值率先更新成了11。因为V对于两个线程都是可见的,线程一要提交的时候把A和V进行对比,发现A=10,而V已经变成了11。所以提交失败。
在这里插入图片描述
在这里插入图片描述4.线程一提交失败后,知道自己的A不是最新的值,于是把A变成最新的V值11,B变成12,V还是11。再次提交,如果没有别的线程又修改了V的值,那么就提交成功,V的值变为12,A的值变成12,B的值变成13。否则,重复最开始的步骤。
在这里插入图片描述
提交成功后。
在这里插入图片描述

这就是一个完整的CAS过程。

CAS在JDK中的实现

CAS在JDK中的实现体现在Unsafe这个类,AtomicInteger等原子类都是基于这个类实现。例如AtomicInteger的incrementAndGet就是调用了Unsafe类的方法。下面的代码是基于JDK8的。高版本的JDK实现是不一样的。

  public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

getAndAddInt方法调用了compareAndSwapInt方法,看名字就知道这就是CAS的实现了。

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

而这个compareAndSwapInt方法是一个native方法,就用深究了。

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

CAS的缺点

1.在高并发的情况下CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2.不能保证代码块的原子性

CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证多个变量共同进行原子性的更新,就不得不使用synchronized了。

3.ABA问题
上面两个缺点前面都提到过,这里汇总一下。接下来特别介绍ABA问题。

ABA问题

假设有一个变量的值为A,第一个线程把值从A变成B,第二个线程把值从B又变回了A,这时候有第三个线程来操作数据,那么他自己就不知道他获取到的这个A到底有没有被修改过。这就是ABA问题,实际上如果只对数据正确性有要求的话,ABA不算什么问题。因为数据并没有出现错误的情况。只是不知道有没有修改过而已。
这个问题实际上就是值有没有被修改的问题,解决办法就是加版本号。JUC包提供了AtomicStampedReference这个类来给值加版本。但我个人觉得这都已经是代码层面的修改了,和CAS的底层实现已经没什么关系了。

CAS的实现

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值