精進之路之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

相關文章
相關標籤/搜索