精进之路之CAS

CAS (Compare And Swap) 即比较交换, 是一种实现并发算法时经常使用到的技术,Java并发包中的不少类都使用了CAS技术,本文将深刻的介绍CAS的原理。java

其算法核心思想以下

    执行函数:CAS(V,E,N)

其包含3个参数

    V表示要更新的变量

    E表示预期值

    N表示新值

       若是V值等于E值,则将V的值设为N。若V值和E值不一样,则说明已经有其余线程作了更新,则当前线程什么都不作。通俗的理解就是CAS操做须要咱们提供一个指望值,当指望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程能够进行修改,也就是执行CAS操做,但若是指望值与当前线程不符,则说明该值已被其余线程修改,此时不执行更新操做,但能够选择从新读取该变量再尝试再次修改该变量,也能够放弃操做,原理图以下


算法



      因为CAS操做属于乐观派,它总认为本身能够成功完成操做,当多个线程同时使用CAS操做一个变量时,只有一个会胜出,并成功更新,其他均会失败,但失败的线程并不会被挂起,仅是被告知失败,而且容许再次尝试,固然也容许失败的线程放弃操做,这点从图中也能够看出来。基于这样的原理,CAS操做即便没有锁,一样知道其余线程对共享资源操做影响,并执行相应的处理措施。同时从这点也能够看出,因为无锁操做中没有锁的存在,所以不可能出现死锁的状况,也就是说无锁操做天生免疫死锁。
并发

 

 

非阻塞算法 (nonblocking algorithms)函数

 

一个线程的失败或者挂起不该该影响其余线程的失败或挂起的算法。this

 

现代的CPU提供了特殊的指令,能够自动更新共享数据,并且可以检测到其余线程的干扰,而 compareAndSet() 就用这些代替了锁定。atom

 

拿出AtomicInteger来研究在没有锁的状况下是如何作到数据正确性的。spa

 

private volatile int value;.net

 

首先毫无觉得,在没有锁的机制下可能须要借助volatile原语,保证线程间的数据是可见的(共享的)。线程

 

这样才获取变量的值的时候才能直接读取。blog

 

public final int get() {
        return value;
    }

 

而后来看看++i是怎么作到的。

 

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

 

在这里采用了CAS操做,每次从内存中读取数据而后将此数据和+1后的结果进行CAS操做,若是成功就返回结果,不然重试直到成功为止。

 

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

 

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

 

总体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。

 

咱们再来看看 java8并发包中的应用

 

以 java.util.concurrent.atomic.AtomicInteger为例,原子方式自增一操做

 

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

 

能够看出,方法中使用了Unsafe 类,调用了 getAndAddInt,compareAndSwapInt

 

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

继而看出,两个方法都是native的
public native int getIntVolatile(Object var1, long var2);
 
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
 

 

CAS的缺点:

CAS虽然很高效的解决了原子操做问题,可是CAS仍然存在三大问题。

    循环时间长开销很大。
    只能保证一个共享变量的原子操做。
    ABA问题。

循环时间长开销很大:

咱们能够看到getAndAddInt方法执行时,若是CAS失败,会一直进行尝试。若是CAS长时间一直不成功,可能会给CPU带来很大的开销。


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

当对一个共享变量执行操做时,咱们可使用循环CAS的方式来保证原子操做,可是对多个共享变量操做时,循环CAS就没法保证操做的原子性,这个时候就能够用锁来保证原子性。


什么是ABA问题?ABA问题怎么解决?

若是内存地址V初次读取的值是A,而且在准备赋值的时候检查到它的值仍然为A,那咱们就能说它的值没有被其余线程改变过了吗?

若是在这段期间它的值曾经被改为了B,后来又被改回为A,那CAS操做就会误认为它历来没有被改变过。这个漏洞称为CAS操做的“ABA”问题。

Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它能够经过控制变量值的版原本保证CAS的正确性。所以,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,若是须要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

---------------------

注:本文参考、整理自  原文:https://blog.csdn.net/mmoren/article/details/79185862,https://blog.csdn.net/v123411739/article/details/79561458

相关文章
相关标签/搜索