基于CAS操作的非阻塞算法

2022-12-14,,,

阻塞算法(non-blocking algorithms)定义
 
     所谓非阻塞算法是相对于锁机制而言的,是指:一个线程的失败或挂起不应该引起另一个线程的失败或挂起的一种算法。一般是利用硬件层面支持的原子化操作指令来取代锁的,比如CAS(compare and swap),从而保证共享数据在并发访问下的数据一致性。
 
由AtomicInteger的线程安全机制说起

 
AtomicInteger内部是如何保证线程同步的呢?我们先看AtomicInteger内部的一个典型的方法:
     public final int getAndSet(int newValue) {
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
   }
     public final boolean compareAndSet (int expect, int update) {
       return unsafe.compareAndSwapInt(thisvalueOffset, expect, update);
   }
 
解释:compareAndSet方法内部是调用Java本地方法compareAndSwapInt来实现的,而compareAndSwapInt方法内部又是借助C来调用CPU的底层指令来保证在硬件层面上实现原子操作的。在intel处理器中,CAS是通过调用cmpxchg指令完成的。这就是我们常说的CAS操作(compare and swap)。CAS操作很容易理解,一般来说有三个值:内存值V,期望值A,更新值B,如果内存值V和期望值A相等,那么就用更新值B替换内存值,否则什么都不做。想象一下,假设AtomicInteger当前的value值为1,某个线程A正在执行上述的getAndSet方法,当执行到compareAndSet方法的时候,被另一个线程B抢占了,线程B成功将内存值更新为2,然后轮到线程A来继续执行上述还没有执行完的比较并更新操作,由于线程A上次获得到的current值是1,然后开始执行compareAndSet方法(最后交由CPU的原子执行来执行的),comareAndSet方法发现当前内存值V=2,而期望值A=1(current变量值),所以就不会产生值交换,然后继续下一次重试,在没有别的线程抢占的情况下,下一个循环(在并发很高的情况下可能经过更多次的循环)线程A就能够设置成功,如果线程A是在还没有运行int current = get()这一行操作时被抢占了,那么线程B运行完毕后,线程A获得的将是线程B修改后的值然后进行CAS操作可能就一次成功(在没有其他线程抢占的情况下)。因此,CAS Try-Loop操作能够很好的提供线程同步机制,我们又将此实现过程称之为线程同步的无阻塞算法,又叫”CAS循环”,”lock-free“或”wait-free“算法。
 
非阻塞算法的优点
 
  在java5.0版本时,我们只能通过synchronized来实现线程的同步,synchronized是一种独占锁,独占锁是一种悲观锁,当一个线程访问共享资源的时候,其他线程必须处在阻塞状态,只有在拥有锁的线程释放锁以后才能被其他线程锁竞争,JVM 实现阻塞的方式通常是挂起阻塞的线程,过一会儿再重新调度它。由此造成的上下文切换相对于锁保护的少数几条指令来说,会造成相当大的延迟,这将引起性能问题,所以我们称这种锁为重量级锁。所谓乐观锁就是指:对竞争资源不用加锁,而是假设没有冲突去完成某项操作,如果因为冲突失败就不断重试,直到成功为止。上面所说的循环CAS操作就是上述所说的乐观锁。

    利用硬件的原生支持代替JVM对代码路径的锁定,从而提供更细粒度(独立的内存地址)的同步。
    失败的线程可以立即重试而不用被挂起,降低了争用成本,即使有少量失败的CAS操作,也依然锁争用造成的重新调度快的多
    争用CAS提供更短的延迟(因为争用CAS比争用锁会更快),提供更好的吞吐率。
    对生存问题(死锁和线程优先级反转)提供更好的防御

在轻度到中度的争用情况下阻塞算法的性能会超越阻塞算法,因为 CAS 的多数时间都在第一次尝试时就成功,而发生争用时的开销也不涉及线程挂起和上下文切换,只多了几个循环迭代。没有争用的 CAS 要比没有争用的锁便宜得多(这句话肯定是真的,因为没有争用的锁涉及 CAS 加上额外的处理),而争用的 CAS 比争用的锁获取涉及更短的延迟。在高度争用的情况下(即有多个线程不断争用一个内存位置的时候),基于锁的算法开始提供比非阻塞算法更好的吞吐率,因为当线程阻塞时,它就会停止争用,耐心地等候轮到自己,从而避免了进一步争用。但是,这么高的争用程度并不常见,因为多数时候,线程会把线程本地的计算与争用共享数据的操作分开,从而给其他线程使用共享数据的机会。
 
参考:
Java 理论与实践: 非阻塞算法简介
Java 理论与实践: 流行的原子
Non-blocking algorithm
ABA problem

基于CAS操作的非阻塞算法的相关教程结束。

《基于CAS操作的非阻塞算法.doc》

下载本文的Word格式文档,以方便收藏与打印。