併發編程—CAS(Compare And Swap)

鎖(lock)的代價

鎖是用來作併發最簡單的方式,其代價也是最高的,Java在JDK1.5以前都是靠synchronized關鍵字來加鎖。可是加鎖機制會有以下幾個問題:java

  • 加鎖、釋放鎖會須要操做系統進行上下文切換和調度延時,在上下文切換的時候,cpu以前緩存的指令和數據都將失效,這個過程將增長系統開銷。
  • 多個線程同時競爭鎖,鎖競爭機制自己須要消耗系統資源。沒有獲取到鎖的線程會被掛起直至獲取鎖,在線程被掛起和恢復執行的過程當中也存在很大開銷。
  • 等待鎖的線程會阻塞,影響實際的使用體驗。若是被阻塞的線程優先級高,而持有鎖的線程優先級低,將會致使優先級反轉(Priority Inversion)。

樂觀鎖與悲觀鎖

悲觀鎖:是認爲別的線程會修改值。
獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖。synchronized加鎖後就可以確保程序執行時不會被其它線程干擾,獲得正確的結果。算法

樂觀鎖:本質上是樂觀的,認爲別的線程不會去修改值。若是發現值被修改了,能夠再次重試。CAS機制(Compare And Swap)就是一種樂觀鎖。編程

Compare And Swap

CAS是一種有名的無鎖(lock-free)算法。也是一種現代 CPU 普遍支持的CPU指令級的操做,只有一步原子操做,因此很是快。並且CAS避免了請求操做系統來裁定鎖的問題,不用麻煩操做系統,直接在CPU內部就搞定了。緩存

CAS有三個操做參數:併發

  1. 內存位置V(它的值是咱們想要去更新的)
  2. 預期原值A(上一次從內存中讀取的值)
  3. 新值B(應該寫入的新值)

CAS的操做過程:將內存位置V的值與A比較(compare),若是相等,則說明沒有其它線程來修改過這個值,因此把內存V的的值更新成B(swap),若是不相等,說明V上的值被修改過了,不更新,而是返回當前V的值,再從新執行一次任務再繼續這個過程。測試

因此,當多個線程嘗試使用CAS同時更新同一個變量時,其中一個線程會成功更新變量的值,剩下的會失敗。失敗的線程能夠重試或者什麼也不作。this

簡單來講,CAS 的含義是「我認爲原有的值應該是什麼,若是是,則將原有的值更新爲新值,不然不作修改,並告訴我這個值如今是多少」。(這段描述引自《Java併發編程實踐》)atom

JVM對CAS的支持

在JDK1.5以前,若是不編寫明確的代碼就沒法執行CAS操做,在JDK1.5中引入了底層的支持,在int、long和對象的引用等類型上都公開了CAS的操做,而且JVM把它們編譯爲底層硬件提供的最有效的方法,在運行CAS的平臺上,運行時把它們編譯爲相應的機器指令,若是處理器/CPU不支持CAS指令,那麼JVM將使用自旋鎖。操作系統

在原子類變量中,如java.util.concurrent.atomic包下的AtomicXXX,都使用了這些底層的JVM支持爲數字類型的引用類型提供一種高效的CAS操做,而在java.util.concurrent中的大多數類在實現時都直接或間接的使用了這些原子變量類。線程

java.util.concurrent.atomic.AtomicLong源碼中的自增getAndIncrement()方法:

//+1操做
    public final long getAndIncrement() {
        while (true) {
            long current = get();
            long next = current + 1;
            //當+1操做成功的時候直接返回,退出此循環
            if (compareAndSet(current, next))
                return current;
        }
    }


    //調用JNI實現CAS
    public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }

JNI:Java Native Interface爲JAVA本地調用,容許java調用其餘語言。在jdk1.8後getAndIncrement()方法已經看不到具體代碼了,而是封裝在unsafe類裏面。

CAS缺點

CAS雖然很高效的解決原子操做,可是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操做。

  1. ABA問題。由於CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是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的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比有兩個共享變量i=2,j=a,合併一下ij=2a,而後用CAS來操做ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。
  4. 比較花費CPU資源,即便沒有任何爭用也會作一些無用功。
  5. 會增長程序測試的複雜度,稍不注意就會出現問題。

總結

能夠用CAS在無鎖的狀況下實現原子操做,但要明確應用場合,很是簡單的操做且又不想引入鎖能夠考慮使用CAS操做,當想要非阻塞地完成某一操做也能夠考慮CAS。不推薦在複雜操做中引入CAS,會使程序可讀性變差,且難以測試,同時會出現ABA問題。

相關文章
相關標籤/搜索