CAS(Compare and Swap)算法介绍、缺陷和解决思路

问题情景:Java中处理原子操作的Atomic系列类(如AtomicInteger,AtomicDouble等)中,处理同步性问题采用的是CAS算法,看了一下感觉算法貌似有点不对劲,然后就搜了搜网上已有的解决思路汇总如下。

鸣谢程序员囧辉JupiterMouse水欣分享博文供笔者参考

CAS是什么

提到同步,第一反应我想到的是synchronized加锁。但是当线程很多并发量很大的时候,这种方式性能会变得极差。于是就有了本文主角:CAS算法。

CAS即Compare and Swap,比较并交换算法,处理同步问题的常见解决思路。需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

比如AtomicInteger的自增操作:

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

自增并返回自增前的值。这个方法又调用了Unsafe类实例的getAndAddInt方法。这个类并不在jdk中,于是我们只能通过IDEA或者Eclipse自带的反编译工具查看,或者下载openJDK一探究竟。反编译结果如下:


    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;
    }

变量全都用varX代替,又没啥注释说明,不易看懂,但整体思路不难理解:var1是Object型,表示要改变的对象;var2是long型,表示偏移量;var4表示要增加的值。

方法中先从内存中拿到var1的最新值赋值到var5,然后尝试将内存位置的值修改为var5+var4(要增加的量),如果修改失败,则获取该内存位置的新值v,然后继续尝试,直至修改成功。

再深入一层的compareAndSwapInt方法就涉及到JNI的东西了,暂时先不管,如有兴趣可以参考文章开头的两篇鸣谢博文。

 

CAS的缺陷

那么下面提一提该算法的缺陷。

1.循环开销大

可以看到,方法内部用不断循环的方式实现修改。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

解决方案

  • 破坏掉for死循环,当超过一定时间或者一定次数时,return退出。JDK8新增的LongAddr,和ConcurrentHashMap类似的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来。
  • 如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空,从而提高CPU的实行效率。

2.只能保证一个共享变量的原子操作

需要对多个共享变量操作时,循环CAS就无法保证操作的原子性。

解决方案

  • 用锁
  • 把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ji=2a,然后用CAS来操作ij。
  • 封装成对象。注:从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之前的原子性,可以把多个变量放在一个对象里来进行CAS操作。

3.ABA问题

CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么CAS进行检查的时候发现它的值没有发生变化,但是实质上它已经发生了改变 。可能会造成数据的缺失。

解决方案

CAS类似于乐观锁,即每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。因此解决方案也可以跟乐观锁一样:

  • 使用版本号机制,如手动增加版本号字段
  • Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前的标志是否等于预期标志,如果全部相等,则以原子方式将该应用和该标志的值设置为给定的更新值。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值