并发编程—CAS(Compare And Swap)

锁(lock)的代价

锁是用来作并发最简单的方式,其代价也是最高的,Java在JDK1.5以前都是靠synchronized关键字来加锁。可是加锁机制会有以下几个问题:java

  • 加锁、释放锁会须要操做系统进行上下文切换和调度延时,在上下文切换的时候,cpu以前缓存的指令和数据都将失效,这个过程将增长系统开销。
  • 多个线程同时竞争锁,锁竞争机制自己须要消耗系统资源。没有获取到锁的线程会被挂起直至获取锁,在线程被挂起和恢复执行的过程当中也存在很大开销。
  • 等待锁的线程会阻塞,影响实际的使用体验。若是被阻塞的线程优先级高,而持有锁的线程优先级低,将会致使优先级反转(Priority Inversion)。

乐观锁与悲观锁

悲观锁:是认为别的线程会修改值。
独占锁是一种悲观锁,synchronized就是一种独占锁。synchronized加锁后就可以确保程序执行时不会被其它线程干扰,获得正确的结果。算法

乐观锁:本质上是乐观的,认为别的线程不会去修改值。若是发现值被修改了,能够再次重试。CAS机制(Compare And Swap)就是一种乐观锁。编程

Compare And Swap

CAS是一种有名的无锁(lock-free)算法。也是一种现代 CPU 普遍支持的CPU指令级的操做,只有一步原子操做,因此很是快。并且CAS避免了请求操做系统来裁定锁的问题,不用麻烦操做系统,直接在CPU内部就搞定了。缓存

CAS有三个操做参数:并发

  1. 内存位置V(它的值是咱们想要去更新的)
  2. 预期原值A(上一次从内存中读取的值)
  3. 新值B(应该写入的新值)

CAS的操做过程:将内存位置V的值与A比较(compare),若是相等,则说明没有其它线程来修改过这个值,因此把内存V的的值更新成B(swap),若是不相等,说明V上的值被修改过了,不更新,而是返回当前V的值,再从新执行一次任务再继续这个过程。测试

因此,当多个线程尝试使用CAS同时更新同一个变量时,其中一个线程会成功更新变量的值,剩下的会失败。失败的线程能够重试或者什么也不作。this

简单来讲,CAS 的含义是“我认为原有的值应该是什么,若是是,则将原有的值更新为新值,不然不作修改,并告诉我这个值如今是多少”。(这段描述引自《Java并发编程实践》)atom

JVM对CAS的支持

在JDK1.5以前,若是不编写明确的代码就没法执行CAS操做,在JDK1.5中引入了底层的支持,在int、long和对象的引用等类型上都公开了CAS的操做,而且JVM把它们编译为底层硬件提供的最有效的方法,在运行CAS的平台上,运行时把它们编译为相应的机器指令,若是处理器/CPU不支持CAS指令,那么JVM将使用自旋锁。操作系统

在原子类变量中,如java.util.concurrent.atomic包下的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操做,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。线程

java.util.concurrent.atomic.AtomicLong源码中的自增getAndIncrement()方法:

//+1操做
    public final long getAndIncrement() {
        while (true) {
            long current = get();
            long next = current + 1;
            //当+1操做成功的时候直接返回,退出此循环
            if (compareAndSet(current, next))
                return current;
        }
    }


    //调用JNI实现CAS
    public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }

JNI:Java Native Interface为JAVA本地调用,容许java调用其余语言。在jdk1.8后getAndIncrement()方法已经看不到具体代码了,而是封装在unsafe类里面。

CAS缺点

CAS虽然很高效的解决原子操做,可是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操做。

  1. ABA问题。由于CAS须要在操做值的时候检查下值有没有发生变化,若是没有发生变化则更新,可是若是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,可是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

    从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法做用是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  2. 循环时间长开销大。自旋CAS若是长时间不成功,会给CPU带来很是大的执行开销。若是JVM能支持处理器提供的pause指令那么效率会有必定的提高,pause指令有两个做用,第一它能够延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它能够避免在退出循环的时候因内存顺序冲突(memory order violation)而引发CPU流水线被清空(CPU pipeline flush),从而提升CPU的执行效率。
  3. 只能保证一个共享变量的原子操做。当对一个共享变量执行操做时,咱们可使用循环CAS的方式来保证原子操做,可是对多个共享变量操做时,循环CAS就没法保证操做的原子性,这个时候就能够用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操做。好比有两个共享变量i=2,j=a,合并一下ij=2a,而后用CAS来操做ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行CAS操做。
  4. 比较花费CPU资源,即便没有任何争用也会作一些无用功。
  5. 会增长程序测试的复杂度,稍不注意就会出现问题。

总结

能够用CAS在无锁的状况下实现原子操做,但要明确应用场合,很是简单的操做且又不想引入锁能够考虑使用CAS操做,当想要非阻塞地完成某一操做也能够考虑CAS。不推荐在复杂操做中引入CAS,会使程序可读性变差,且难以测试,同时会出现ABA问题。

相关文章
相关标签/搜索