Java中的樂觀鎖——無鎖策略

LJWLgl發表於2018-11-25

題主在閱讀《實戰Java高併發程式設計》一書時,瞭解到了Java無鎖的相關概念,在此記錄下來以加深對其的理解,Java中的鎖分為兩種即為悲觀鎖和樂觀鎖,那麼何為悲觀鎖和樂觀鎖呢? 點選檢視原文

樂觀鎖與悲觀鎖

悲觀鎖是我們程式碼經常用到的,比如說Java中的synchronizedReentrantLock等獨佔鎖就是悲觀鎖思想的實現,它總是假設別的執行緒在拿資料的時候都會修改資料,所以在每次拿到資料的時候都會上鎖,這樣別的執行緒想拿這個資料就會被阻塞直到它拿到鎖。
樂觀鎖與之相反,它總是假設別的執行緒取資料的時候不會修改資料,所以不會上鎖,但是會在更新的時候判斷有沒有更新過資料。也就是,樂觀鎖(無鎖)使用一種比較交換的技術(CAS Compare And Swap)來鑑別執行緒衝突,一旦檢測到衝突的產生,就重試當前操作直到沒有衝突的產生。
與鎖相比,使用比較交換(CAS)會使程式碼看起來更加複雜一些。但由於其非阻塞性,它對死鎖問題天生免疫,並且執行緒之間的相互影響也遠遠比基於鎖的方式要小。更為重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統開銷,也沒有執行緒之間頻繁排程帶來的開銷,因此,它要比基於鎖的方式擁有更優越的效能。

樂觀鎖實現

樂視鎖的實現之一就是CAS演算法,CAS演算法的過程大致是這樣的:它包含三個引數CAS(V, E, N)。

  • V表示要更新的變數
  • E表示預期值
  • N表示新值 僅當V等於E的時候,才會把V的值設定成N,否則不會執行任何操作(比較和替換是一個原子操作)。如果V值和E值不相等,則說明有其他執行緒修改過V值,當前執行緒什麼都不做,最後返回當前V的真實值。CAS操作是抱著樂觀的態度進行的,它總是認為自己可以成功的完成操作。當多個執行緒同時使用CAS操作一個變數時,只有一個會成功更新,其餘都會失敗。失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。

樂觀鎖在JDK中的應用

java.util.concurrent.atomic包下面的原子變數類就是使用了CAS來實現的,下面我們重點看一下CAS在該包下面的AtomicInteger類實際應用,該類提供下面幾個核心方法和屬性:

  • public final int incrementAndGet() // 當前值加1,返回舊值
  • public final int decrementAndGet() // 當前值減1,返回舊值
  • public volatile int value // AtomicInteger物件當前實際取值

incrementAndGet()decrementAndGet()方法類似,我們只看一下incrementAndGet方法就好,JDK1.7與JDK1.8在實現incrementAndGet()方法有所區別(Java8中CAS的增強),下面給出的是在java8中的實現,可以看到incrementAndGet()實際呼叫的是sun.misc.Unsafe.getAndAddInt方法,Unsafe類可以理解為Java中指標,但是我們不可以直接使用,因為它是由Bootstrap類載入器載入,而非AppLoader載入。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;  // 
}
複製程式碼

程式碼中的valueOffset代表value欄位在AtomicInteger物件中的偏移量(到物件頭部的偏移量),方便快速定位欄位。

public final int getAndAddInt(Object obj, long l, int i)
{
    int j;
    do
        j = getIntVolatile(obj, l);
    while(!compareAndSwapInt(obj, l, j, j + i));
    return j;
}
複製程式碼

傳入getAndAddInt方法的引數分別是obj(AtomicInteger物件)、l(物件內偏移量)、i(增加值),可以看到getAndAddInt實際是一個迴圈,只有compareAndSwapInt返回true時,迴圈才能結束,並返回j(舊值),下面是compareAndSwapInt方法簽名,其中前面兩個引數和傳入getAndAddInt方法引數一致,後面expected的值是通過getIntVolatile獲取的舊值,x是希望設定的新值。

public final native boolean compareAndSwapInt(Object obj, long offset, int expected, int x);
複製程式碼

與compareAndSwapInt方法類似,getIntVolatile()內部也是用原子操作獲取AtomicInteger物件的value值,下面是該方法的簽名

public native int getIntVolatile(Object obj, long l);
複製程式碼

CAS在JDK原始碼中應用廣泛,下面給出其餘的無鎖的類:

  • AtomicReference 無鎖的物件引用
  • AtomicStampedReference 帶有標誌的物件引用
  • AtomicIntegerArray 無鎖的陣列
  • AtomicIntegerFieldUpdater 無鎖的普通變數

樂觀鎖的問題

ABA問題
如果一個變數V初次讀取是A值,並且在準備賦值的時候也是A值,那就能說明A值沒有被修改過嗎?其實是不能的,因為變數V可能被其他執行緒改回A值,結果就是會導致CAS操作誤認為從來沒被修改過,從而賦值給V。 JDK 1.5以後提供了上文所說的AtomicStampedReference類來解決了這個問題,其中compareAndSet方法會首先檢查當前引用是否等於預期引用,其次檢查當前標誌是否等於預期標誌,如果都相等就會以原子的方式將引用和標誌都設定為新值。

自旋時間長
CAS自旋就是上文說的getAndAddInt()方法內部do-while迴圈,如果compareAndSwapInt一直未設定成功,do-while一直迴圈下去,會給CPU帶來非常大的執行開銷。網上給出執行方法如下,unchecked(還沒試過~)

如果JVM能支援處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

只能保證單個共享變數
CAS操作只對單個共享變數有用,涉及多個變數時無法使用CAS,同樣在JDK 1.5之後,提供了AtomicReference物件引用,可以多個變數放到一個AtomicReference物件裡。

使用場景

簡單的來說CAS適用於寫比較少的情況下(多讀場景,衝突一般較少),synchronized適用於寫比較多的情況下(多寫場景,衝突一般較多)

參考文件

相關文章