CAS (全 ) && concurrent包的实现

在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁。
锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。

CAS(乐观锁的实现):
当多个线程使用CAS获取锁,只能有一个成功,其他线程返回失败,继续尝试获取锁;
CAS操作中包含三个参数:V(需读写的内存位置的值)+A(准备用来比较的参数)+B(准备写入的新值):若A的参数与V值相匹配,就写入B;若不匹配,就不进行操作。

JAVA中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个CAS。 java.util.concurrent 包下的大量类都使用了这个 Unsafe.java 类的CAS操作(下文我们会讲到concurrent包的具体实现)。

我们先拿AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。

private volatile int value;
首先,在没有锁的机制下可能需要借助volatile原语,保证线程间的数据是可见的(共享的)。这样才获取变量的值的时候才能直接读取。
public final int get() {
        return value;
    }
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

compareAndSet利用JNI来完成CPU指令的操作。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

java.util.concurrent.atomic包中对CAS的实现是通过synchronized关键字实现的:

public final synchronized boolean compareAndSet(long expect, long update) { 
        if (value == expect) {                                                  
            value = update;                                                    
            return true;                                                        
        } else {                                                                
            return false;                                                       
        }                                                                      
    }

整体的过程就是:利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。
CAS会导致ABA的问题:
ABA问题:
在运用CAS做Lock-Free操作中有一个经典的ABA问题:

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题,例如下面的例子:
在这里插入图片描述
现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:
在这里插入图片描述
此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:
在这里插入图片描述
其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

自旋CAS的基本思路就是循环进行CAS操作,直到成功为止。

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

(1)ABA问题。如果一个值原来是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就无法保证操作的原子性了。但从JDK1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

concurrent包的实现

由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

  • A线程写volatile变量,随后B线程读这个volatile变量。
    A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
    A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
    A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
    Java的CAS使用的是现代处理器上提供的高效机器级别原子指令,这些原子指令以原子的方式对内存进行读改写的操作,这是在多处理器下实现同步的关键。
    同时,volatile关键字修饰的变量的读写和CAS之间可以实现线程之间的通信。
    以上两种特性整合在一起,就形成了整个concurrent包得以实现的基石。我们通过concurrent包的源代码实现可以发现一个模式:
    1.首先声明共享变量volatile;
    2.然后使用CAS的原子条件更新来实现线程之间的同步;
    3.同时,配合以volatile的读写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS 非阻塞数据结构和原子变量类(Atomic类)都是java.util.concurrent.atomic包中的类,这些基础类都是使用上述模式实现的,concurrent包中的高层类又是依赖于这些基础类来实现的。
在这里插入图片描述
总结
可以用CAS在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用CAS操作,当想要非阻塞地完成某一操作也可以考虑CAS。不推荐在复杂操作中引入CAS,会使程序可读性变差,且难以测试,同时会出现ABA问题。

之后的文章中我们将详细讲解Atomic相关知识以及AQS(AbstractQueuedSynchronizer)的用法。

猜你喜欢

转载自blog.csdn.net/mulinsen77/article/details/84575603
Cas
今日推荐