1. 程式人生 > >java多執行緒系列3:悲觀鎖和樂觀鎖

java多執行緒系列3:悲觀鎖和樂觀鎖

1.悲觀鎖和樂觀鎖的基本概念

悲觀鎖:

  1. 總是認為當前想要獲取的資源存在競爭(很悲觀的想法),因此獲取資源後會立刻加鎖,於是其他執行緒想要獲取該資源的時候就會一直阻塞直到能夠獲取到鎖;
  2. 在傳統的關係型資料庫中,例如行鎖、表鎖、讀鎖、寫鎖等,都用到了悲觀鎖。還有java中的同步關鍵字Synchronized也是一種悲觀鎖;

樂觀鎖:

  1. 總是認為當前想要獲取的資源不存在競爭(很樂觀的想法),因此在獲取資源後,並不會加鎖;
  2. 但是在執行更新操作時,會判斷在這期間是否有其他人更新過這個資料,可使用版本號等機制實現;
  3. 適用於多讀的應用程式,可提高吞吐量;
  4. 像資料庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變數類就是使用了樂觀鎖的一種實現方式CAS實現的。

2.樂觀鎖的一種實現方式:CAS

  因為樂觀鎖的思想是:在通常情況下都認為不會產生併發衝突,因此在對資料進行提交更新的時候,會對將要提交更新的資料進行併發衝突檢測、如果衝突存在,則會返回錯誤資訊給使用者,讓使用者決定處理方式。

  基於樂觀鎖的思想,我們可以知道樂觀鎖實現的步驟包含兩個部分:衝突檢測和資料更新,而CAS就是其中一個典型的實現方式.

  CAS:Compare And Swap(比較並交換)

    CAS是一種樂觀鎖技術。當多個執行緒使用CAS嘗試更新同一個變數時,只有一個執行緒能夠成功更新,其他執行緒都會失敗,但是失敗的執行緒並不會掛起,而是被告知在此次競爭中失敗並可再次嘗試。

    CAS包含三個運算元:

    

  在JDK1.5中新增的java.util.concurrent包中的內容就是建立早CAS基礎之上的,相對於Synchronized的阻塞式演算法,CAS其實是一種非阻塞演算法的實現,因此java.util.concurrent包中元件的效能大大提升。

  下面以java.util.concurrent中的AtomicInteger的getAndIncrement(該操作相當於變數自加) 為例,看一下在不加鎖的情況下,如何保證執行緒安全:

public class AtomicInteger extends Number implements
java.io.Serializable { private volatile int value; public final int get() { return value; } public final int getAndIncrement() { //自旋方式採用CAS來修改當前值,直到成功為止 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); } }

如果compareAndSet(current, next)方法成功執行,則直接返回;如果執行緒競爭激烈,導致compareAndSet(current, next)方法一直不能成功執行,則會一直迴圈等待。

3.CAS存在的問題

  ①. ABA問題。因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

  

  ②.迴圈時間開銷大:因為CAS中存在自旋,當自旋長時間不成功時,會給CPU帶來極大開銷,如果CPU執行支援pause指令,效率能夠得到提升。

    pause指令作用1:延遲流水線執行指令,(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。

    pause指令作用2:可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

  ③.只能保證一個共享變數的原子操作。當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,你可以把多個變數放在一個物件裡來進行CAS操作。

4.CAS與Synchronized的選擇

1、執行緒衝突嚴重時,使用CAS等樂觀鎖,自旋機率較大,會因為自旋浪費更多的CPU資源;此時使用Synchronized等悲觀鎖效能較好。2、執行緒衝突較輕時,使用synchronized同步鎖進行執行緒阻塞和喚醒切換以及使用者態核心態間的切換操作額外浪費消耗cpu資源,而自旋概率較小,使用CAS效能高於同步鎖。