Java併發問題--樂觀鎖與悲觀鎖以及樂觀鎖的一種實現方式-CAS

Java併發問題--樂觀鎖與悲觀鎖以及樂觀鎖的一種實現方式-CAS

首先介紹一些樂觀鎖與悲觀鎖:html

  悲觀鎖:老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。再好比Java裏面的同步原語synchronized關鍵字的實現也是悲觀鎖。java

  樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。算法

 

樂觀鎖的一種實現方式-CAS(Compare and Swap 比較並交換):數據庫

  鎖存在的問題:安全

    Java在JDK1.5以前都是靠 synchronized關鍵字保證同步的,這種經過使用一致的鎖定協議來協調對共享狀態的訪問,能夠確保不管哪一個線程持有共享變量的鎖,都採用獨佔的方式來訪問這些變量。這就是一種獨佔鎖,獨佔鎖其實就是一種悲觀鎖,因此能夠說 synchronized 是悲觀鎖。數據結構

    悲觀鎖機制存在如下問題:  多線程

      1. 在多線程競爭下,加鎖、釋放鎖會致使比較多的上下文切換和調度延時,引發性能問題。併發

      2. 一個線程持有鎖會致使其它全部須要此鎖的線程掛起。異步

      3. 若是一個優先級高的線程等待一個優先級低的線程釋放鎖會致使優先級倒置,引發性能風險。性能

    對比於悲觀鎖的這些問題,另外一個更加有效的鎖就是樂觀鎖。其實樂觀鎖就是:每次不加鎖而是假設沒有併發衝突而去完成某項操做,若是由於併發衝突失敗就重試,直到成功爲止。

  樂觀鎖:

    樂觀鎖( Optimistic Locking )在上文已經說過了,其實就是一種思想。相對悲觀鎖而言,樂觀鎖假設認爲數據通常狀況下不會產生併發衝突,因此在數據進行提交更新的時候,纔會正式對數據是否產生併發衝突進行檢測,若是發現併發衝突了,則讓返回用戶錯誤的信息,讓用戶決定如何去作。

    上面提到的樂觀鎖的概念中其實已經闡述了它的具體實現細節:主要就是兩個步驟:衝突檢測和數據更新。其實現方式有一種比較典型的就是 Compare and Swap ( CAS )。

  CAS:

    CAS是樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。   

    CAS 操做中包含三個操做數 —— 須要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。若是內存位置V的值與預期原值A相匹配,那麼處理器會自動將該位置值更新爲新值B。不然處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該位置的值。(在 CAS 的一些特殊狀況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了「 我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。 」這其實和樂觀鎖的衝突檢查+數據更新的原理是同樣的。

    這裏再強調一下,樂觀鎖是一種思想。CAS是這種思想的一種實現方式。

  JAVA對CAS的支持:

    在JDK1.5 中新增 java.util.concurrent (J.U.C)就是創建在CAS之上的。相對於對於 synchronized 這種阻塞算法,CAS是非阻塞算法的一種常見實現。因此J.U.C在性能上有了很大的提高。

    以 java.util.concurrent 中的 AtomicInteger 爲例,看一下在不使用鎖的狀況下是如何保證線程安全的。主要理解 getAndIncrement 方法,該方法的做用至關於 ++i 操做。

public class AtomicInteger extends Number implements java.io.Serializable {  
    private volatile int value; 

    public final int get() {  
        return value;  
    }  

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

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

 

    在沒有鎖的機制下,字段value要藉助volatile原語,保證線程間的數據是可見性。這樣在獲取變量的值的時候才能直接讀取。而後來看看 ++i 是怎麼作到的。

     getAndIncrement 採用了CAS操做,每次從內存中讀取數據而後將此數據和 +1 後的結果進行CAS操做,若是成功就返回結果,不然重試直到成功爲止。

     而 compareAndSet 利用JNI(Java Native Interface)來完成CPU指令的操做:

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

 

    其中unsafe.compareAndSwapInt(this, valueOffset, expect, update);相似以下邏輯:

if (this == expect) {
    this = update
    return true;
} else {
    return false;
}

    那麼比較this == expect,替換this = update,compareAndSwapInt實現這兩個步驟的原子性呢? 參考CAS的原理

  CAS原理:

    CAS經過調用JNI的代碼實現的。而compareAndSwapInt就是藉助C來調用CPU底層指令實現的。

      下面從分析比較經常使用的CPU(intel x86)來解釋CAS的實現原理。

      下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:

public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

     能夠看到這是個本地方法調用。這個本地方法在JDK中依次調用的C++代碼爲:

#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

    如上面源代碼所示,程序會根據當前處理器的類型來決定是否爲cmpxchg指令添加lock前綴。若是程序是在多處理器上運行,就爲cmpxchg指令加上lock前綴(lock cmpxchg)。反之,若是程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不須要lock前綴提供的內存屏障效果)。

 

  CAS缺點:

     1. ABA問題:

       好比說一個線程one從內存位置V中取出A,這時候另外一個線程two也從內存中取出A,而且two進行了一些操做變成了B,而後two又將V位置的數據變成A,這時候線程one進行CAS操做發現內存中仍然是A,而後one操做成功。儘管線程one的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丟掉了。

       從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

public boolean compareAndSet(
     V   expectedReference,//預期引用

     V      newReference,//更新後的引用

     int    expectedStamp, //預期標誌

     int    newStamp //更新後的標誌
)

        實際應用代碼:

private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
........
atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);

 

     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操做。

 

  CAS與Synchronized的使用情景:   

    一、對於資源競爭較少(線程衝突較輕)的狀況,使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操做額外浪費消耗cpu資源;而CAS基於硬件實現,不須要進入內核,不須要切換線程,操做自旋概率較少,所以能夠得到更高的性能。

    二、對於資源競爭嚴重(線程衝突嚴重)的狀況,CAS自旋的機率會比較大,從而浪費更多的CPU資源,效率低於synchronized。

   補充: synchronized在jdk1.6以後,已經改進優化。synchronized的底層實現主要依靠Lock-Free的隊列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但得到了高吞吐量。在線程衝突較少的狀況下,能夠得到和CAS相似的性能;而線程衝突嚴重的狀況下,性能遠高於CAS。

 

  concurrent包的實現:

    因爲java的CAS同時具備 volatile 讀和volatile寫的內存語義,所以Java線程之間的通訊如今有了下面四種方式:

      1. A線程寫volatile變量,隨後B線程讀這個volatile變量。

      2. A線程寫volatile變量,隨後B線程用CAS更新這個volatile變量。

      3. A線程用CAS更新一個volatile變量,隨後B線程用CAS更新這個volatile變量。

      4. A線程用CAS更新一個volatile變量,隨後B線程讀這個volatile變量。

    Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內存執行讀-改-寫操做,這是在多處理器中實現同步的關鍵(從本質上來講,可以支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,所以任何現代的多處理器都會去支持某種能對內存執行原子性讀-改-寫操做的原子指令)。同時,volatile變量的讀/寫和CAS能夠實現線程之間的通訊。把這些特性整合在一塊兒,就造成了整個concurrent包得以實現的基石。若是咱們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:

      1. 首先,聲明共享變量爲volatile;  

      2. 而後,使用CAS的原子條件更新來實現線程之間的同步;

      3. 同時,配合以volatile的讀/寫和CAS所具備的volatile讀和寫的內存語義來實現線程之間的通訊。

    AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從總體來看,concurrent包的實現示意圖以下:

      

 

  JVM中的CAS(堆中對象的分配): 

    Java調用new object()會建立一個對象,這個對象會被分配到JVM的堆中。那麼這個對象究竟是怎麼在堆中保存的呢?

    首先,new object()執行的時候,這個對象須要多大的空間,實際上是已經肯定的,由於java中的各類數據類型,佔用多大的空間都是固定的(對其原理不清楚的請自行Google)。那麼接下來的工做就是在堆中找出那麼一塊空間用於存放這個對象。 
    在單線程的狀況下,通常有兩種分配策略:

      1. 指針碰撞:這種通常適用於內存是絕對規整的(內存是否規整取決於內存回收策略),分配空間的工做只是將指針像空閒內存一側移動對象大小的距離便可。

      2. 空閒列表:這種適用於內存非規整的狀況,這種狀況下JVM會維護一個內存列表,記錄哪些內存區域是空閒的,大小是多少。給對象分配空間的時候去空閒列表裏查詢到合適的區域而後進行分配便可。

    可是JVM不可能一直在單線程狀態下運行,那樣效率太差了。因爲再給一個對象分配內存的時候不是原子性的操做,至少須要如下幾步:查找空閒列表、分配內存、修改空閒列表等等,這是不安全的。解決併發時的安全問題也有兩種策略:

      1. CAS:實際上虛擬機採用CAS配合上失敗重試的方式保證更新操做的原子性,原理和上面講的同樣。

      2. TLAB:若是使用CAS其實對性能仍是會有影響的,因此JVM又提出了一種更高級的優化策略:每一個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝區(TLAB),線程內部須要分配內存時直接在TLAB上分配就行,避免了線程衝突。只有當緩衝區的內存用光須要從新分配內存的時候纔會進行CAS操做分配更大的內存空間。 
      虛擬機是否使用TLAB,能夠經過-XX:+/-UseTLAB參數來進行配置(jdk5及之後的版本默認是啓用TLAB的)。

相關文章
相關標籤/搜索