Java并发基础(14):CAS原理

目录

写在前面

1、乐观锁

2、CAS

3、JAVA对CAS的支持

4、CAS缺陷

5、CAS与Synchronized的使用情景:   


写在前面

        CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术

1、乐观锁

                CAS加volatile关键字是实现并发包的基石。没有CAS就不会有并发包,synchronized是一种独占锁、悲观锁,java.util.concurrent中借助了CAS指令实现了一种区别于synchronized的一种乐观锁。

jdk1.5之前锁存在的问题:

        java在1.5之前都是靠synchronized关键字保证同步,synchronized保证了无论哪个线程持有共享变量的锁,都会采用独占的方式来访问这些变量。

这种情况下:

  • 在多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题
  • 如果一个线程持有锁,其他的线程就都会挂起,等待持有锁的线程释放锁。
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置引起性能风险

        对比于悲观锁的这些问题,另一个更加有效的锁就是乐观锁。

        乐观锁就是:每次不加锁而是假设没有并发冲突去操作同一变量,如果有并发冲突导致失败,则重试直至成功。乐观锁概念为,每次拿数据的时候都认为别的线程不会修改这个数据,所以不会上锁,但是在更新的时候会判断一下在此期间别的线程有没有修改过数据,乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。

2、CAS

        乐观锁主要就是两个步骤:冲突检测和数据更新。当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程可以更新变量的值,其他的线程都会失败,失败的线程并不会挂起,而是告知这次竞争中失败了,并可以再次尝试。

        CAS全称CompareAndSwap,比较交换,主要是通过处理器的指令保证操作原子性。包含三个操作数:

  1. 变量内存地址,V表示;
  2. 旧的预期值,A表示;
  3. 新值,B表示;

当执行CAS指令时,只有当V=A时,才会用B去更新V,否则不执行更新操作。

  1.  假设线程1和线程2同时访问变量V=33,两个线程将变量值33拷贝到各自工作空间内存。
  2. 两线程分别+1,分别得到准备设置的新值34,而后对V进行CAS操作。
  3. 线程1执行成功,将值设为34,完成后更新自己本地值A=34。
  4. 线程2操作返回失败,因为V的值已经被线程1设置为34
  5. 线程2失败后进行重试,获取34到本地,+1,进行CAS(34,34,35),线程2设置成功。

        CAS操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。如果内存位置与预期原值的A相匹配,那么将内存位置的值更新为新值B。如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。

        CAS其实就是一个:我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。这其实和乐观锁的冲突检测+数据更新的原理是一样的。

乐观锁是一种思想,CAS只是这种思想的一种实现方式。

3、JAVA对CAS的支持

        并发包中的原子操作类AtomicInteger来看下,如何在不使用锁的情况下保证线程安全,主要看下getAndIncrement方法,相当于i++的操作:

public class AtomicInteger extends Number implements java.io.Serializable {  
    private volatile int value; 
 
    public final int get() {  
        return value;  
    }  
 
    public final int getAndIncrement() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return current;  
        }  
    }  
 
    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
}

        首先value使用了volatile修饰,这就保证了他的可见性与有序性getAndIncrement采用CAS操作,每次从内存中读取数据然后将数据进行+1操作,然后对原数据,+1后的结果进行CAS操作,成功的话返回结果,否则重试直到成功为止。其中调用了compareAndSet利用JNI(java navite Interface navite修饰的方法,都是java调用其他语言的方法来实现的)来完成CPU的操作。

4、CAS缺陷

        1、ABA问题:在CAS更新过程中,当读取到的值为A,然后准备赋值时仍为A,但实际上有可能A的值被改成了B然后又被改回了A,叫做ABA问题。只是该问题在大部分场景不影响并发的最终效果。

解决办法:Java使用AtomicStampedReference解决该问题,他加入了预期标记和更新后的标记两个字段,更新时不光检查值还要检查当前的标志是否等于预期标志,全部相等时才会更新。

 可以看出AtomicStampedRefenrence类中出了对象引用reference,还加入了标志字段stamp来解决ABA问题。

2、循环时间开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大开销。

上图线程2如果更新失败,会进行重试,采用自旋方式进行重试,如果有多个线程操作共享变量时,部分线程可能自旋时间过长,对CPU造成很大开销。

3、只能保证一个共享变量的原则操作:只对一个共享变量操作可以保证原子性,但是多个不行。多个可以使用AtomicReference或Synchronized实现。

5、CAS与Synchronized的使用情景:   

    1、对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

    2、对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

   补充: synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

参考:面试必问的CAS,你懂了吗?_程序员囧辉-CSDN博客_cas面试

CAS原理分析_漫步夕阳下的博客-CSDN博客_cas原理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值