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

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

樂觀鎖與悲觀鎖

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

樂觀鎖實現

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

  • 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對象中的偏移量(到對象頭部的偏移量),方便快速定位字段。bash

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值,下面是該方法的簽名ide

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適用於寫比較多的狀況下(多寫場景,衝突通常較多)

參考文檔

相關文章
相關標籤/搜索