1. 程式人生 > >Java併發問題--樂觀鎖與悲觀鎖以及樂觀鎖的一種實現方式-CAS

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

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

  悲觀鎖:總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。再比如Java裡面的同步原語synchronized關鍵字的實現也是悲觀鎖。

  樂觀鎖:顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制。樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫提供的類似於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 操作。

複製程式碼

 1 public class AtomicInteger extends Number implements java.io.Serializable {  
 2     private volatile int value; 
 3 
 4     public final int get() {  
 5         return value;  
 6     }  
 7 
 8     public final int getAndIncrement() {  
 9         for (;;) {  
10             int current = get();  
11             int next = current + 1;  
12             if (compareAndSet(current, next))  
13                 return current;  
14         }  
15     }  
16 
17     public final boolean compareAndSet(int expect, int update) {  
18         return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
19     }  
20 }

複製程式碼

    在沒有鎖的機制下,欄位value要藉助volatile原語,保證執行緒間的資料是可見性。這樣在獲取變數的值的時候才能直接讀取。然後來看看 ++i 是怎麼做到的。

     getAndIncrement 採用了CAS操作,每次從記憶體中讀取資料然後將此資料和 +1 後的結果進行CAS操作,如果成功就返回結果,否則重試直到成功為止。

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

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

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

1 if (this == expect) {
2     this = update
3     return true;
4 } else {
5     return false;
6 }

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

  CAS原理:

    CAS通過呼叫JNI的程式碼實現的。而compareAndSwapInt就是藉助C來呼叫CPU底層指令實現的。

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

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

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

     可以看到這是個本地方法呼叫。這個本地方法在JDK中依次呼叫的C++程式碼為:

複製程式碼

 1 #define LOCK_IF_MP(mp) __asm cmp mp, 0  \
 2                        __asm je L0      \
 3                        __asm _emit 0xF0 \
 4                        __asm L0:
 5 
 6 inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
 7   // alternative for InterlockedCompareExchange
 8   int mp = os::is_MP();
 9   __asm {
10     mov edx, dest
11     mov ecx, exchange_value
12     mov eax, compare_value
13     LOCK_IF_MP(mp)
14     cmpxchg dword ptr [edx], ecx
15   }
16 }

複製程式碼

    如上面原始碼所示,程式會根據當前處理器的型別來決定是否為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方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。

複製程式碼

1 public boolean compareAndSet(
2                V      expectedReference,//預期引用
3 
4                V      newReference,//更新後的引用
5 
6               int    expectedStamp, //預期標誌
7 
8               int    newStamp //更新後的標誌
9 ) 

複製程式碼

        實際應用程式碼:

1 private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
2 
3 ........
4 
5 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的使用情景:   

    1、對於資源競爭較少(執行緒衝突較輕)的情況,使用synchronized同步鎖進行執行緒阻塞和喚醒切換以及使用者態核心態間的切換操作額外浪費消耗cpu資源;而CAS基於硬體實現,不需要進入核心,不需要切換執行緒,操作自旋機率較少,因此可以獲得更高的效能。

    2、對於資源競爭嚴重(執行緒衝突嚴重)的情況,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的)。

相關推薦

【轉】Java併發問題--樂觀悲觀以及樂觀實現方式-CAS

首先介紹一些樂觀鎖與悲觀鎖: 悲觀鎖:總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上

Java併發問題--樂觀悲觀以及樂觀實現方式-CAS

首先介紹一些樂觀鎖與悲觀鎖:   悲觀鎖:總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

Java並發問題--樂觀悲觀以及樂觀實現方式-CAS

RF -- 指針 locking water 更多 錯誤 創建 判斷 首先介紹一些樂觀鎖與悲觀鎖: 悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。傳統的關系型數據庫裏邊就用到了很多這

樂觀實現方式——CAS

www. 提升 中一 num 對象 用戶 ace 另一個 nbsp 原文出處: hollischuang (@Hollis_Chuang) 在深入理解樂觀鎖與悲觀鎖一文中我們介紹過鎖。本文在這篇文章的基礎上,深入分析一下樂觀鎖的實現機制,介紹什麽是CAS、CAS的應用以及C

java 多執行緒的實現方式

private ThreadPoolExecutor threadPoolExecutor; /** * 獲取執行緒池 * @return */ private ThreadPoolExecutor getThreadPoolExecutor(){

Java框架之Spring AOP 面向切面程式設計 有哪幾實現方式?如何選擇適合的AOP實現方式

文章目錄 1. 實現方式 2. JDK動態代理如何實現? 2.1 主要的實現過程 3. 如何選擇? 1. 實現方式 JDK 動態代理實現和 cglib 實現 2. JDK

Java併發問題--樂觀悲觀

首先為什麼需要鎖(併發控制)? 在多使用者環境中,在同一時間可能會有多個使用者更新相同的記錄,這會產生衝突。這就是著名的併發性問題。 典型的衝突有: 丟失更新:一個事務的更新覆蓋了其它事務的更新結果,就是所謂的更新丟失。例如:使用者A把值從6改為2,使用者B把值從2改為

Java多線程系列---“基礎篇”13之 樂觀悲觀

而是 關系型 lock color 情況 發現 mis 再次 中一 轉自:http://www.cnblogs.com/zhengbin/p/5657435.html 樂觀鎖   樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿數據的時候都認

併發控制中的樂觀悲觀

為什麼需要鎖(併發控制)? 在多使用者環境中,在同一時間可能會有多個使用者更新相同的記錄,這會產生衝突。這就是著名的併發性問題。 典型的衝突有: (1)丟失更新:一個事務的更新覆蓋了其它事務的更新結果,就是所謂的更新丟失。例如:使用者A把值從6改為2,使用

基於Django的樂觀悲觀解決訂單併發問題的一點淺見

然後就是樂觀鎖查詢了,相比悲觀鎖,樂觀鎖其實並不能稱為是鎖,那麼它是在做什麼事情呢。其實是在你要進行資料庫操作時先去查詢一次資料庫中商品的庫存,然後在你要更新資料庫中商品庫存時,將你一開始查詢到的庫存數量和商品的ID一起作為更新的條件,當受影響行數返回為0時,說明沒有修改成功,那麼就是說別的程序修改了該資料,

Java併發程式設計(05):悲觀樂觀機制

本文原始碼:[GitHub·點這裡](https://github.com/cicadasmile/java-base-parent) || [GitEE·點這裡](https://gitee.com/cicadasmile/java-base-parent) # 一、資源和加鎖 ## 1、場景描述

樂觀悲觀

到你 目前 from 提高 選中 base 排它鎖 之前 準備 在多用戶環境中,在同一時間可能會有多個用戶更新相同的記錄,這會產生沖突。這就是著名的並發性問題。 典型的沖突有: l 丟失更新:一個事務的更新覆蓋了其它事務的更新結果,就是所謂的更新丟失。例如:用戶A把值從6改

樂觀悲觀的簡單區分

個數 行數 但是 分布式系 修改 讀寫 使用場景 狀態 控制 1、鎖的出現,是因為並發讀寫同一個數據的時候,需要進行數據完備性的保護,避免臟讀、臟寫等。 2、樂觀鎖,需要在事務中加鎖,在讀取數據的時候,不必在意數據是否已經被修改了(即允許臟讀);但是在寫入數據的時候,要檢查

[數據庫事務]詳解七: 深入理解樂觀悲觀

ood insert 影響 hiberna memcach begin 策略 goods 其它 註明: 本文轉載自http://www.hollischuang.com/archives/934在數據庫的鎖機制中介紹過,數據庫管理系統(DBMS)中的並發控制的任務是確保在

mysql的樂觀悲觀

想要 附加 情況 屬性 ... str 但是 share 版本 樂觀鎖 總是認為不會產生並發問題,每次去取數據的時候總認為不會有其他線程對數據進行修改,因此不會上鎖,但是在更新時會判斷其他線程在這之前有沒有對數據進行修改,一般會使用版本號機制或CAS操作實現。 例如: 有這

深入理解樂觀悲觀

遇到 實現 個數 默認 ODB date 開始 安全 行數 前言在數據庫的鎖機制中介紹過,數據庫管理系統(DBMS)中的並發控制的任務是確保在多個事務同時存取數據庫中同一數據時不破壞事務的隔離性和統一性以及數據庫的統一性。 樂觀並發控制(樂觀鎖)和悲觀並發控制(悲觀鎖)是並

資料庫中的樂觀悲觀

樂觀鎖: 在關係資料庫管理系統裡,樂觀併發控制(又名“樂觀鎖”,Optimistic Concurrency Control,縮寫“OCC”)是一種併發控制的方法。它假設多使用者併發的事務在處理時不會彼此互相影響,各事務能夠在不產生鎖的情況下處理各自影響的那部分資料。在提交資料更新之前,

資料庫 樂觀悲觀

樂觀鎖 總是認為不會產生併發問題,每次去取資料的時候總認為不會有其他執行緒對資料進行修改,因此不會上鎖,但是在更新時會判斷其他執行緒在這之前有沒有對資料進行修改,一般會使用版本號機制或CAS操作實現。 version方式:一般是在資料表中加上一個資料版本號version欄位,表示資料被修改的次數,當

最通俗易懂的樂觀悲觀原理及實現

一、樂觀鎖   總是認為不會產生併發問題,每次去取資料的時候總認為不會有其他執行緒對資料進行修改,因此不會上鎖,但是在更新時會判斷其他執行緒在這之前有沒有對資料進行修改,一般會使用版本號機制或CAS操作實現。  version方式:一般是在資料表中加上一個資料版本號ver

什麼是樂觀悲觀

樂觀鎖: 簡單的來說:就是認為別人不會過來修改它的資料,常見的樂觀鎖通常會帶一個version(版本),等到提交的時候,會去檢查一下版本,如果版本修改了,就會丟擲異常,並且回滾資料; 樂觀鎖通常需要在表中額外設計一個version的冗餘欄位 並且在插入資料的時候,將version初始