《面試補習》- Java鎖知識大梳理

黃老吉發表於2020-03-09

面試補習系列:

一、鎖的分類

1、樂觀鎖和悲觀鎖

樂觀鎖就是樂觀的認為不會發生衝突,用cas和版本號實現 悲觀鎖就是認為一定會發生衝突,對操作上鎖

1.悲觀鎖

悲觀鎖,總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。

傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。
再比如 Java 裡面的同步原語 synchronized 關鍵字的實現也是悲觀鎖。
複製程式碼

適用場景:

比較適合寫入操作比較頻繁的場景,如果出現大量的讀取操作,每次讀取的時候都會進行加鎖,這樣會增加大量的鎖的開銷,降低了系統的吞吐量。

實現方式: synchronizedLock

2.樂觀鎖

每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制

ABA問題(JDK1.5之後已有解決方案):CAS需要在操作值的時候檢查記憶體值是否發生變化,沒有發生變化才會更新記憶體值。但是如果記憶體值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。ABA問題的解決思路就是在變數前面新增版本號,每次變數更新的時候都把版本號加一,這樣變化過程就從“A-B-A”變成了“1A-2B-3A”。

迴圈時間長開銷大:CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。

只能保證一個共享變數的原子操作(JDK1.5之後已有解決方案):對一個共享變數執行操作時,CAS能夠保證原子操作,但是對多個共享變數操作時,CAS是無法保證操作的原子性的。
複製程式碼

適用場景:

比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,資料發生衝突的可能性就會增大,為了保證資料的一致性,應用層需要不斷的重新獲取資料,這樣會增加大量的查詢操作,降低了系統的吞吐量。

實現方式:

1、使用版本標識來確定讀到的資料與提交時的資料是否一致。提交後修改版本標識,不一致時可以採取丟棄和再次嘗試的策略。

2、Java 中的 Compare and Swap 即 CAS ,當多個執行緒嘗試使用 CAS 同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。

3、在 Java 中 java.util.concurrent.atomic 包下面的原子變數類就是使用了樂觀鎖的一種實現方式 CAS 實現的。

2、公平鎖/非公平鎖

公平鎖:

指多個執行緒按照申請鎖的順序來獲取鎖。
複製程式碼

非公平鎖:

指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲取鎖。
有可能,會造成優先順序反轉或者飢餓現象。

複製程式碼

擴充執行緒飢餓:

一個或者多個執行緒因為種種原因無法獲得所需要的資源,導致一直無法執行的狀態
導致無法獲取的原因:
執行緒優先順序較低,沒辦法獲取cpu時間
其他執行緒總是能在它之前持續地對該同步塊進行訪問。
執行緒在等待一個本身也處於永久等待完成的物件(比如呼叫這個物件的 wait 方法),因為其他執行緒總是被持續地獲得喚醒。

複製程式碼

實現方式: ReenTrantLock(公平/非公平)

對於Java ReentrantLock而言,通過建構函式指定該鎖是否是公平鎖,預設是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。

對於Synchronized而言,也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS(AbstractQueuedSynchronizer)的來實現執行緒排程,所以並沒有任何辦法使其變成公平鎖。

3、可重入鎖

如果一個執行緒獲得過該鎖,可以再次獲得,主要是用途就是在遞迴方面,還有就是防止死鎖,比如在一個同步方法塊中呼叫了另一個相同鎖物件的同步方法塊

實現方式: synchronizedReentrantLock

4、獨享鎖/共享鎖

獨享鎖是指該鎖一次只能被一個執行緒所持有。
共享鎖是指該鎖可被多個執行緒所持有。
複製程式碼

實現方式: 獨享鎖: ReentrantLocksynchronized 貢獻鎖: ReadWriteLock

擴充:

互斥鎖/讀寫鎖 就是對上面的一種具體實現:

互斥鎖:在Java中的具體實現就是ReentrantLock,synchronized
讀寫鎖:在Java中的具體實現就是ReadWriteLock
複製程式碼

對於Java ReentrantLock而言,其是獨享鎖。但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。對於Synchronized而言,當然是獨享鎖

5、偏向鎖/輕量級鎖/重量級鎖

基於 jdk 1.6 以上

偏向鎖指的是當前只有這個執行緒獲得,沒有發生爭搶,此時將方法頭的markword設定成0,然後每次過來都cas一下就好,不用重複的獲取鎖.指一段同步程式碼一直被一個執行緒所訪問,那麼該執行緒會自動獲取鎖。降低獲取鎖的代價

輕量級鎖:在偏向鎖的基礎上,有執行緒來爭搶,此時膨脹為輕量級鎖,多個執行緒獲取鎖時用cas自旋獲取,而不是阻塞狀態

重量級鎖:輕量級鎖自旋一定次數後,膨脹為重量級鎖,其他執行緒阻塞,當獲取鎖執行緒釋放鎖後喚醒其他執行緒。(執行緒阻塞和喚醒比上下文切換的時間影響大的多,涉及到使用者態和核心態的切換)

實現方式: synchronized

6、分段鎖

在1.7的concurrenthashmap中有分段鎖的實現,具體為預設16個的segement陣列,其中segement繼承自reentranklock,每個執行緒過來獲取一個鎖,然後操作這個鎖下連著的map。

實現方式:

我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱為Segment,
它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry陣列,陣列中的每個元素又是一個連結串列;
同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。

當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,
然後對這個分段進行加鎖,所以當多執行緒put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。

但是,在統計size的時候,可就是獲取hashmap全域性資訊的時候,就需要獲取所有的分段鎖才能統計。

分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個陣列的時候,就僅僅針對陣列中的一項進行加鎖操作。
複製程式碼

二、鎖的底層實現

1、Synchronized

synchronized 關鍵字通過一對位元組碼指令 monitorenter/monitorexit 實現

前置知識:

物件頭:
Hotspot 虛擬機器的物件頭主要包括兩部分資料:Mark Word(標記欄位)、Klass Pointer(型別指標)。其中:

Klass Point 是是物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

Mark Word 用於儲存物件自身的執行時資料,它是實現輕量級鎖和偏向鎖的關鍵,所以下面將重點闡述 Mark Word 。

Monitor:
每一個 Java 物件都有成為Monitor 的潛質,因為在 Java 的設計中 ,每一個 Java 物件自打孃胎裡出來就帶了一把看不見的鎖,它叫做內部鎖或者 Monitor 鎖

複製程式碼

物件頭結構:

《面試補習》- Java鎖知識大梳理

Monitor資料結構:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的執行緒,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的執行緒,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
參考: https://blog.csdn.net/javazejian/article/details/72828483
複製程式碼

ObjectMonitor中有兩個佇列,_WaitSet_EntryList,用來儲存ObjectWaiter物件列表( 每個等待鎖的執行緒都會被封裝成ObjectWaiter物件),_owner指向持有ObjectMonitor物件的執行緒,當多個執行緒同時訪問一段同步程式碼時,首先會進入 _EntryList 集合,當執行緒獲取到物件的monitor 後進入 _Owner 區域並把monitor中的owner變數設定為當前執行緒同時monitor中的計數器count加1.

若執行緒呼叫 wait() 方法,將釋放當前持有的monitor,owner變數恢復為null,count自減1,同時該執行緒進入 WaitSe t集合中等待被喚醒。若當前執行緒執行完畢也將釋放monitor(鎖)並復位變數的值,以便其他執行緒進入獲取monitor(鎖)

這裡比較複雜,但是建議仔細閱讀,便於後續分析的時候理解
複製程式碼

《面試補習》- Java鎖知識大梳理

1.1、位元組碼實現

同步程式碼塊:
public class SynchronizedTest {

    public void test2() {
        synchronized(this) {
        }
    }
}
複製程式碼

《面試補習》- Java鎖知識大梳理

synchronized關鍵字基於上述兩個指令實現了鎖的獲取和釋放過程:

monitorenter指令插入到同步程式碼塊的開始位置,

monitorexit 指令插入到同步程式碼塊的結束位置.

執行緒執行到 monitorenter 指令時,將會嘗試獲取物件所對應的 Monitor 所有權,即嘗試獲取物件的鎖。

當執行monitorenter指令時,當前執行緒將試圖獲取 objectref(即物件鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器為 0,那執行緒可以成功取得 monitor,並將計數器值設定為 1,取鎖成功。如果當前執行緒已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。倘若其他執行緒已經擁有 objectref 的 monitor 的所有權,那當前執行緒將被阻塞,直到正在執行執行緒執行完畢,即monitorexit指令被執行,執行執行緒將釋放 monitor(鎖)並設定計數器值為0 ,其他執行緒將有機會持有 monitor 。

複製程式碼
同步方法:
synchronized 方法則會被翻譯成普通的方法呼叫和返回指令如:
invokevirtual、areturn 指令,在 JVM 位元組碼層面並沒有任何特別的指令來實現被synchronized 修飾的方法,
而是在 Class 檔案的方法表中將該方法的 access_flags 欄位中的 synchronized 標誌位置設定為 1,
表示該方法是同步方法,並使用呼叫該方法的物件或該方法所屬的 Class 
在 JVM 的內部物件表示 Klass 作為鎖物件
複製程式碼
 //省略沒必要的位元組碼
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"
複製程式碼

以下部分參考: JVM原始碼分析之synchronized實現

1.2、偏向鎖獲取

1、獲取物件頭的Mark Word;
2、判斷mark是否為可偏向狀態,即mark的偏向鎖標誌位為 1,鎖標誌位為 01;
3、判斷mark中JavaThread的狀態:如果為空,則進入步驟(4);如果指向當前執行緒,
則執行同步程式碼塊;如果指向其它執行緒,進入步驟(5);
4、通過CAS原子指令設定mark中JavaThread為當前執行緒ID,
如果執行CAS成功,則執行同步程式碼塊,否則進入步驟(5);
5、如果執行CAS失敗,表示當前存在多個執行緒競爭鎖,當達到全域性安全點(safepoint),
獲得偏向鎖的執行緒被掛起,撤銷偏向鎖,並升級為輕量級,升級完成後被阻塞在安全點的執行緒繼續執行同步程式碼塊;
複製程式碼

在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,因此為了減少同一執行緒獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個執行緒獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個執行緒再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程式的效能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個執行緒申請相同的鎖。

注意 JVM 提供了關閉偏向鎖的機制, JVM 啟動命令指定如下引數即可

-XX:-UseBiasedLocking
複製程式碼

《面試補習》- Java鎖知識大梳理

偏向鎖的撤銷:

偏向鎖的 撤銷(revoke) 是一個很特殊的操作, 為了執行撤銷操作, 需要等待全域性安全點(Safe Point), 
此時間點所有的工作執行緒都停止了位元組碼的執行。

偏向鎖這個機制很特殊, 別的鎖在執行完同步程式碼塊後, 都會有釋放鎖的操作, 而偏向鎖並沒有直觀意義上的“釋放鎖”操作。

引入一個概念 epoch, 其本質是一個時間戳 , 代表了偏向鎖的有效性
複製程式碼

1.3、輕量級鎖

在多執行緒交替執行同步塊的情況下,儘量避免重量級鎖引起的效能消耗,但是如果多個執行緒在同一時刻進入臨界區,會導致輕量級鎖膨脹升級重量級鎖,所以輕量級鎖的出現並非是要替代重量級鎖。

1、獲取物件的markOop資料mark;
2、判斷mark是否為無鎖狀態:mark的偏向鎖標誌位為 0,鎖標誌位為 01;
3、如果mark處於無鎖狀態,則進入步驟(4),否則執行步驟(6);
4、把mark儲存到BasicLock物件的_displaced_header欄位;
5、通過CAS嘗試將Mark Word更新為指向BasicLock物件的指標,如果更新成功,表示競爭到鎖,則執行同步程式碼,否則執行步驟(6);
6、如果當前mark處於加鎖狀態,且mark中的ptr指標指向當前執行緒的棧幀,則執行同步程式碼,否則說明有多個執行緒競爭輕量級鎖,輕量級鎖需要膨脹升級為重量級鎖;

複製程式碼

1.4、重量級鎖

重量級鎖通過物件內部的監視器(monitor)實現,其中monitor的本質是依賴於底層作業系統的Mutex Lock實現,作業系統實現執行緒之間的切換需要從使用者態到核心態的切換,切換成本非常高。

鎖膨脹過程:

1、整個膨脹過程在自旋下完成;
2、mark->has_monitor()方法判斷當前是否為重量級鎖,即Mark Word的鎖標識位為 10,如果當前狀態為重量級鎖,執行步驟(3),否則執行步驟(4);
3、mark->monitor()方法獲取指向ObjectMonitor的指標,並返回,說明膨脹過程已經完成;
4、如果當前鎖處於膨脹中,說明該鎖正在被其它執行緒執行膨脹操作,則當前執行緒就進行自旋等待鎖膨脹完成,這裡需要注意一點,
雖然是自旋操作,但不會一直佔用cpu資源,每隔一段時間會通過os::NakedYield方法放棄cpu資源,或通過park方法掛起;
如果其他執行緒完成鎖的膨脹操作,則退出自旋並返回;
5、如果當前是輕量級鎖狀態,即鎖標識位為 00

複製程式碼

Monitor 競爭:

1、通過CAS嘗試把monitor的_owner欄位設定為當前執行緒;
2、如果設定之前的_owner指向當前執行緒,說明當前執行緒再次進入monitor,即重入鎖,執行_recursions ++ ,記錄重入的次數;
3、如果之前的_owner指向的地址在當前執行緒中,這種描述有點拗口,換一種說法:之前_owner指向的BasicLock在當前執行緒棧上,
說明當前執行緒是第一次進入該monitor,設定_recursions為1,_owner為當前執行緒,該執行緒成功獲得鎖並返回;
4、如果獲取鎖失敗,則等待鎖的釋放;

複製程式碼

其本質就是通過CAS設定monitor的_owner欄位為當前執行緒,如果CAS成功,則表示該執行緒獲取了鎖,跳出自旋操作,執行同步程式碼,否則繼續被掛起;

Monitor 釋放:

當某個持有鎖的執行緒執行完同步程式碼塊時,會進行鎖的釋放,給其它執行緒機會執行同步程式碼,在HotSpot中,通過退出monitor的方式實現鎖的釋放,並通知被阻塞的執行緒.

1.5、鎖優化內容

鎖消除:

消除鎖是虛擬機器另外一種鎖的優化,這種優化更徹底,
Java虛擬機器在JIT編譯時(可以簡單理解為當某段程式碼即將第一次被執行時進行編譯,又稱即時編譯),
通過對執行上下文的掃描,去除不可能存在共享資源競爭的鎖,
通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間
複製程式碼

鎖粗化:

將多個連續的加鎖、解鎖操作連線在一起,擴充套件成一個範圍更大的鎖。
複製程式碼

自旋鎖:

執行緒的阻塞和喚醒,需要 CPU 從使用者態轉為核心態。頻繁的阻塞和喚醒對 CPU 來說是一件負擔很重的工作,勢必會給系統的併發效能帶來很大的壓力。
同時,我們發現在許多應用上面,物件鎖的鎖狀態只會持續很短一段時間。為了這一段很短的時間,頻繁地阻塞和喚醒執行緒是非常不值得的

適應性自旋鎖:
自適應就意味著自旋的次數不再是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定
複製程式碼

鎖升級:

《面試補習》- Java鎖知識大梳理

2、ReetrantLock

2.1、Lock

《面試補習》- Java鎖知識大梳理

   //加鎖
    void lock();

    //解鎖
    void unlock();

    //可中斷獲取鎖,與lock()不同之處在於可響應中斷操作,即在獲
    //取鎖的過程中可中斷,注意synchronized在獲取鎖時是不可中斷的
    void lockInterruptibly() throws InterruptedException;

    //嘗試非阻塞獲取鎖,呼叫該方法後立即返回結果,如果能夠獲取則返回true,否則返回false
    boolean tryLock();

    //根據傳入的時間段獲取鎖,在指定時間內沒有獲取鎖則返回false,如果在指定時間內當前執行緒未被中並斷獲取到鎖則返回true
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    //獲取等待通知元件,該元件與當前鎖繫結,當前執行緒只有獲得了鎖
    //才能呼叫該元件的wait()方法,而呼叫後,當前執行緒將釋放鎖。
    Condition newCondition();

複製程式碼

在Java 1.5中,官方在concurrent併發包(J.U.C)中加入了Lock介面,該介面中提供了lock()方法和unLock()方法對顯式加鎖和顯式釋放鎖操作進行支援.

Lock 鎖提供的優勢:

可以使鎖更公平。
可以使執行緒在等待鎖的時候響應中斷。
可以讓執行緒嘗試獲取鎖,並在無法獲取鎖的時候立即返回或者等待一段時間。
可以在不同的範圍,以不同的順序獲取和釋放鎖。
複製程式碼

2.2、AQS (AbstractQueuedSynchronizer)

AQS 即佇列同步器。它是構建鎖或者其他同步元件的基礎框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等),J.U.C 併發包的作者(Doug Lea)期望它能夠成為實現大部分同步需求的基礎。

資料結構:

    //同步佇列頭節點
    private transient volatile Node head;

    //同步佇列尾節點
    private transient volatile Node tail;

    //同步狀態
    private volatile int state;
複製程式碼

AQS 使用一個 int 型別的成員變數 state 來表示同步狀態:

  • state > 0 時,表示已經獲取了鎖。
  • state = 0 時,表示釋放了鎖。

Node構成FIFO的同步佇列來完成執行緒獲取鎖的排隊工作

  • 如果當前執行緒獲取同步狀態失敗(鎖)時,AQS 則會將當前執行緒以及等待狀態等資訊構造成一個節點(Node)並將其加入同步佇列,同時會阻塞當前執行緒
  • 當同步狀態釋放時,則會把節點中的執行緒喚醒,使其再次嘗試獲取同步狀態

參考: 深入剖析基於併發AQS的(獨佔鎖)重入鎖(ReetrantLock)及其Condition實現原理

《面試補習》- Java鎖知識大梳理

2.3、Sync

Sync:抽象類,是ReentrantLock的內部類,繼承自AbstractQueuedSynchronizer,實現了釋放鎖的操作(tryRelease()方法),並提供了lock抽象方法,由其子類實現。

NonfairSync:是ReentrantLock的內部類,繼承自Sync,非公平鎖的實現類。

FairSync:是ReentrantLock的內部類,繼承自Sync,公平鎖的實現類。

AQS、Sync 和 ReentrantLock 的具體關係圖:

《面試補習》- Java鎖知識大梳理

2.4、ReentrantLock 實現原理

建構函式:

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼

ReentrantLock 提供兩種實現方式,公平鎖/非公平鎖. 通過建構函式進行初始化 sync 進行判斷當前鎖得型別.

2.4.1、非公平鎖(NonfairSync)
    final void lock() {
    //cas 獲取鎖
        if (compareAndSetState(0, 1))
        //如果成功設定當前執行緒Id
            setExclusiveOwnerThread(Thread.currentThread());
        else
        //否則再次請求同步狀態
            acquire(1);
    }
複製程式碼

先對同步狀態執行CAS操作,嘗試把state的狀態從0設定為1, 如果返回true則代表獲取同步狀態成功,也就是當前執行緒獲取鎖成,可操作臨界資源,如果返回false,則表示已有執行緒持有該同步狀態(其值為1) 獲取鎖失敗,注意這裡存在併發的情景,也就是可能同時存在多個執行緒設定state變數,因此是CAS操作保證了state變數操作的原子性。返回false後,執行acquire(1)方法

#acquire(int arg)方法,為 AQS 提供的模板方法。該方法為獨佔式獲取同步狀態,但是該方法對中斷不敏感。也就是說,由於執行緒獲取同步狀態失敗而加入到 CLH 同步佇列中,後續對該執行緒進行中斷操作時,執行緒不會從 CLH 同步佇列中移除。

acquire 程式碼:

   public final void acquire(int arg) {
   //嘗試獲取同步狀態
       if (!tryAcquire(arg) &&
           //自旋直到獲得同步狀態成功,新增節點到佇列    
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
   }
複製程式碼

1、tryAcquire 嘗試獲取同步狀態

    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //鎖閒置
            if (c == 0) {
            //CAS佔用
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果鎖state=1 && 執行緒為當前執行緒 重入鎖的邏輯
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
複製程式碼

2、acquireQueued 加入佇列中,自旋獲取鎖

private Node addWaiter(Node mode) {
   //將請求同步狀態失敗的執行緒封裝成結點
   Node node = new Node(Thread.currentThread(), mode);

   Node pred = tail;
   //如果是第一個結點加入肯定為空,跳過。
   //如果非第一個結點則直接執行CAS入隊操作,嘗試在尾部快速新增
   if (pred != null) {
       node.prev = pred;
       //使用CAS執行尾部結點替換,嘗試在尾部快速新增
       if (compareAndSetTail(pred, node)) {
           pred.next = node;
           return node;
       }
   }
   //如果第一次加入或者CAS操作沒有成功執行enq入隊操作
   enq(node);
   return node;
}

   final boolean acquireQueued(final Node node, int arg) {
       boolean failed = true;
       try {
           boolean interrupted = false;
           for (;;) {
           //獲取前驅節點
               final Node p = node.predecessor();
               //如果前驅節點試頭節點, 嘗試獲取同步狀態
               if (p == head && tryAcquire(arg)) {
                   setHead(node);
                   p.next = null; // help GC
                   failed = false;
                   return interrupted;
               }
               // 獲取失敗,執行緒等待
               if (shouldParkAfterFailedAcquire(p, node) &&
                   parkAndCheckInterrupt())
                   interrupted = true;
           }
       } finally {
           if (failed)
               cancelAcquire(node);
       }
   }

複製程式碼

流程圖:

《面試補習》- Java鎖知識大梳理

2.4.2、公平鎖(FairSync)

與非公平鎖不同的是,在獲取鎖的時,公平鎖的獲取順序是完全遵循時間上的FIFO規則,也就是說先請求的執行緒一定會先獲取鎖,後來的執行緒肯定需要排隊,這點與前面我們分析非公平鎖的nonfairTryAcquire(int acquires)方法實現有鎖不同,下面是公平鎖中tryAcquire()方法的實現

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
        //判斷佇列中是否又執行緒在等待
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //重入鎖邏輯
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
複製程式碼

2.4.3、解鎖

//ReentrantLock類的unlock
public void unlock() {
    sync.release(1);
}

//AQS類的release()方法
public final boolean release(int arg) {
    //嘗試釋放鎖
    if (tryRelease(arg)) {

        Node h = head;
        if (h != null && h.waitStatus != 0)
            //喚醒後繼結點的執行緒
            unparkSuccessor(h);
        return true;
    }
    return false;
}

//ReentrantLock類中的內部類Sync實現的tryRelease(int releases) 
protected final boolean tryRelease(int releases) {

      int c = getState() - releases;
      if (Thread.currentThread() != getExclusiveOwnerThread())
          throw new IllegalMonitorStateException();
      boolean free = false;
      //判斷狀態是否為0,如果是則說明已釋放同步狀態
      if (c == 0) {
          free = true;
          //設定Owner為null
          setExclusiveOwnerThread(null);
      }
      //設定更新同步狀態
      setState(c);
      return free;
  }
複製程式碼

3、ReentrantReadWriteLock

建構函式:

Lock readLock();

Lock writeLock();

/** 使用預設(非公平)的排序屬性建立一個新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
    this(false);
}

/** 使用給定的公平策略建立一個新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

複製程式碼

java.util.concurrent.locks.ReentrantReadWriteLock,實現 ReadWriteLock 介面,可重入的讀寫鎖實現類。在它內部,維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。只要沒有 Writer 執行緒,讀取鎖可以由多個 Reader 執行緒同時保持。也就說說,寫鎖是獨佔的,讀鎖是共享的。

在 ReentrantLock 中,使用 Sync ( 實際是 AQS )的 int 型別的 state 來表示同步狀態,表示鎖被一個執行緒重複獲取的次數。但是,讀寫鎖 ReentrantReadWriteLock 內部維護著一對讀寫鎖,如果要用一個變數維護多種狀態,需要採用“按位切割使用”的方式來維護這個變數,將其切分為兩部分:高16為表示讀,低16為表示寫。

分割之後,讀寫鎖是如何迅速確定讀鎖和寫鎖的狀態呢?通過位運算。假如當前同步狀態為S,那麼:

  • 寫狀態,等於 S & 0x0000FFFF(將高 16 位全部抹去)
  • 讀狀態,等於 S >>> 16 (無符號補 0 右移 16 位)。

《面試補習》- Java鎖知識大梳理

1、readLock

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    
    protected final int tryAcquireShared(int unused) {
    //當前執行緒
    Thread current = Thread.currentThread();
    int c = getState();
    //exclusiveCount(c)計算寫鎖
    //如果存在寫鎖,且鎖的持有者不是當前執行緒,直接返回-1
    //存在鎖降級問題,後續闡述
    if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
        return -1;
    //讀鎖
    int r = sharedCount(c);

    /*
     * readerShouldBlock():讀鎖是否需要等待(公平鎖原則)
     * r < MAX_COUNT:持有執行緒小於最大數(65535)
     * compareAndSetState(c, c + SHARED_UNIT):設定讀取鎖狀態
     */
    if (!readerShouldBlock() &&
            r < MAX_COUNT &&
            compareAndSetState(c, c + SHARED_UNIT)) { //修改高16位的狀態,所以要加上2^16
        /*
         * holdCount部分後面講解
         */
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}
    
    
複製程式碼

4、synchronized 和 ReentrantLock 異同?

相同點

都實現了多執行緒同步和記憶體可見性語義。
都是可重入鎖。
複製程式碼

不同點

同步實現機制不同
synchronized 通過 Java 物件頭鎖標記和 Monitor 物件實現同步。
ReentrantLock 通過CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用於阻塞和解除阻塞)實現同步。


可見性實現機制不同
synchronized 依賴 JVM 記憶體模型保證包含共享變數的多執行緒記憶體可見性。
ReentrantLock 通過 ASQ 的 volatile state 保證包含共享變數的多執行緒記憶體可見性。

使用方式不同
synchronized 可以修飾例項方法(鎖住例項物件)、靜態方法(鎖住類物件)、程式碼塊(顯示指定鎖物件)。
ReentrantLock 顯示呼叫 tryLock 和 lock 方法,需要在 finally 塊中釋放鎖。

功能豐富程度不同
synchronized 不可設定等待時間、不可被中斷(interrupted)。
ReentrantLock 提供有限時間等候鎖(設定過期時間)、可中斷鎖(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等豐富功能

鎖型別不同
synchronized 只支援非公平鎖。
ReentrantLock 提供公平鎖和非公平鎖實現。當然,在大部分情況下,非公平鎖是高效的選擇。
複製程式碼

相關文章