CAS无锁实现原理以及ABA问题

       CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。实现非阻塞同步的方案称为“无锁编程算法”( Non-blocking algorithm)。

为什么要用CAS

       在多线程高并发编程的时候,最关键的问题就是保证临界区的对象的安全访问。通常是用加锁来处理,其实加锁本质上是将并发转变为串行来实现的,势必会影响吞吐量。而且线程的数量是有限的,依赖于操作系统,而且线程的创建和销毁带来的性能损耗是不可以忽略掉的。虽然现在基本都是用线程池来尽可能的降低不断创建线程带来的性能损耗。对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问时没有冲突的,既然没有冲突就不需要等待,线程不需要阻塞。那多个线程共同访问临界区的资源怎么办呢,无锁的策略采用一种比较交换技术CAS(compare and swap)来鉴别线程冲突,一旦检测到冲突,就重试当前操作直到没有冲突为止。

       与锁相比,CAS会使得程序设计比较复杂,但是由于其优越的性能优势,以及天生免疫死锁(根本就没有锁,当然就不会有线程一直阻塞了),更为重要的是,使用无锁的方式没有所竞争带来的开销,也没有线程间频繁调度带来的开销大,他比基于锁的方式有更优越的性能,所以在目前被广泛应用,我们在程序设计时也可以适当的使用。不过由于CAS编码确实稍微复杂,而且jdk作者本身也不希望你直接使用unsafe(后面会讲到)来进行代码的编写,所以如果不能深刻理解CAS以及unsafe还是要慎用,使用一些别人已经实现好的无锁类或者框架就好了。

CAS原理分析

       一个CAS方法包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期的值,N表示新值。只有当V的值等于E时,才会将V的值修改为N。如果V的值不等于E,说明已经被其他线程修改了,当前线程可以放弃此操作,也可以再次尝试次操作直至修改成功。基于这样的算法,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理。

      额外引申技术点:上面说到当前线程可以发现其他线程对临界区数据的修改,这点可以使用volatile进行保证。volatile实现了JMM中的可见性。使得对临界区资源的修改可以马上被其他线程看到,它是通过添加内存屏障实现的。具体实现原理请自行搜索volatile

ABA 问题

       由于 CAS 设计机制就是获取某两个时刻(初始预期值和当前内存值)变量值,并进行比较更新,所以说如果在获取初始预期值和当前内存值这段时间间隔内,变量值由 A 变为 B 再变为 A,那么对于 CAS 来说是不可感知的,但实际上变量已经发生了变化;解决办法是在每次获取时加版本号,并且每次更新对版本号 +1,这样当发生 ABA 问题时通过版本号可以得知变量被改动过。JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长开销大

       所谓循环时间长开销大问题就是当 CAS 判定变量被修改了以后则放弃本次修改,但往往为了保证数据正确性该计算会以循环的方式再次发起 CAS,如果多次 CAS 判定失败,则会产生大量的时间消耗和性能浪费;如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

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

  1. CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效;从 JDK 1.5开始提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作
  2. Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
  3. valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的。
  4. value是用volatile修饰的,保证了多线程之间看到的value值是同一份。

在java领域的广泛应用

jdk中的CAS实现java.util.concurrent.atomic包,该包下的类都是采用CAS来实现的无锁。

posted @ 2018-11-28 15:02  如.若  阅读(2999)  评论(0编辑  收藏  举报