ReentrantLock 源碼分析從入門到入土

回答一個問題

在開始本篇文章的內容講述前,先來回答我一個問題,爲何 JDK 提供一個 synchronized 關鍵字以後還要提供一個 Lock 鎖,這不是畫蛇添足嗎?難道 JDK 設計人員都是沙雕嗎?html

我聽過一句話很是的經典,也是我認爲是每一個人都應該瞭解的一句話:你覺得的並非你覺得的。明白什麼意思麼?不明白的話,加我微信我告訴你。java

初識 ReentrantLock

ReentrantLock 位於 java.util.concurrent.locks 包下,它實現了 Lock 接口和 Serializable 接口。面試

ReentrantLock 是一把可重入鎖互斥鎖,它具備與 synchronized 關鍵字相同的含有隱式監視器鎖(monitor)的基本行爲和語義,可是它比 synchronized 具備更多的方法和功能。微信

ReentrantLock 基本方法

構造方法

ReentrantLock 類中帶有兩個構造函數,一個是默認的構造函數,不帶任何參數;一個是帶有 fair 參數的構造函數數據結構

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

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

第二個構造函數也是判斷 ReentrantLock 是不是公平鎖的條件,若是 fair 爲 true,則會建立一個公平鎖的實現,也就是 new FairSync(),若是 fair 爲 false,則會建立一個 非公平鎖的實現,也就是 new NonfairSync(),默認的狀況下建立的是非公平鎖多線程

// 建立的是公平鎖
private ReentrantLock lock = new ReentrantLock(true);

// 建立的是非公平鎖
private ReentrantLock lock = new ReentrantLock(false);

// 默認建立非公平鎖
private ReentrantLock lock = new ReentrantLock();
複製代碼

FairSync 和 NonfairSync 都是 ReentrantLock 的內部類,繼承於 Sync 類,下面來看一下它們的繼承結構,便於梳理。併發

abstract static class Sync extends AbstractQueuedSynchronizer {...}

static final class FairSync extends Sync {...}
  
static final class NonfairSync extends Sync {...}
複製代碼

在多線程嘗試加鎖時,若是是公平鎖,那麼鎖獲取的機會是相同的。不然,若是是非公平鎖,那麼 ReentrantLock 則不會保證每一個鎖的訪問順序函數

下面是一個公平鎖的實現工具

public class MyFairLock extends Thread{

    private ReentrantLock lock = new ReentrantLock(true);
    public void fairLock(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()  + "正在持有鎖");
        }finally {
            System.out.println(Thread.currentThread().getName()  + "釋放了鎖");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        MyFairLock myFairLock = new MyFairLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "啓動");
            myFairLock.fairLock();
        };
        Thread[] thread = new Thread[10];
        for(int i = 0;i < 10;i++){
            thread[i] = new Thread(runnable);
        }
        for(int i = 0;i < 10;i++){
            thread[i].start();
        }
    }
}
複製代碼

不信?不信你輸出試試啊!懶得輸出?就知道你懶得輸出,因此直接告訴你結論吧,結論就是本身試源碼分析

試完了嗎?試完了我是不會讓你休息的,過來再試一下非公平鎖的測試和結論,知道怎麼試嗎?上面不是講過要給 ReentrantLock 傳遞一個參數的嗎?你想,傳 true 的時候是公平鎖,那麼反過來不就是非公平鎖了?其餘代碼還用改嗎?不須要了啊。

明白了吧,再來測試一下非公平鎖的流程,看看是否是你想要的結果。

公平鎖的加鎖(lock)流程詳解

一般狀況下,使用多線程訪問公平鎖的效率會很是低(一般狀況下會慢不少),可是 ReentrantLock 會保證每一個線程都會公平的持有鎖,線程飢餓的次數比較小。鎖的公平性並不能保證線程調度的公平性。

此時若是你想了解更多的話,那麼我就從源碼的角度跟你聊聊如何 ReentrantLock 是如何實現這兩種鎖的。

如上圖所示,公平鎖的加鎖流程要比非公平鎖的加鎖流程簡單,下面要聊一下具體的流程了,請小夥伴們備好板凳。

下面先看一張流程圖,這張圖是 acquire 方法的三條主要流程

首先是第一條路線,tryAcquire 方法,顧名思義嘗試獲取,也就是說能夠成功獲取鎖,也能夠獲取鎖失敗。

使用 ctrl+左鍵 點進去是調用 AQS 的方法,可是 ReentrantLock 實現了 AQS 接口,因此調用的是 ReentrantLock 的 tryAcquire 方法;

首先會取得當前線程,而後去讀取當前鎖的同步狀態,還記得鎖的四種狀態嗎?分別是 無鎖、偏向鎖、輕量級鎖和重量級鎖,若是你不是很明白的話,請參考博主這篇文章(不懂什麼是鎖?看看這篇你就明白了),若是判斷同步狀態是 0 的話,就證實是無鎖的,參考下面這幅圖( 1bit 表示的是是否偏向鎖 )

若是是無鎖(也就是沒有加鎖),說明是第一次上鎖,首先會先判斷一下隊列中是否有比當前線程等待時間更長的線程(hasQueuedPredecessors);而後經過 CAS 方法原子性的更新鎖的狀態,CAS 方法更新的要求涉及三個變量,currentValue(當前線程的值),expectedValue(指望更新的值),updateValue(更新的值),它們的更新以下

if(currentValue == expectedValue){
  currentValue = updateValue
}
複製代碼

CAS 經過 C 底層機制保證原子性,這個你不須要考慮它。若是既沒有排隊的線程並且使用 CAS 方法成功的把 0 -> 1 (偏向鎖),那麼當前線程就會得到偏向鎖,記錄獲取鎖的線程爲當前線程。

而後咱們看 else if 邏輯,若是讀取的同步狀態是1,說明已經線程獲取到了鎖,那麼就先判斷當前線程是否是獲取鎖的線程,若是是的話,記錄一下獲取鎖的次數 + 1,也就是說,只有同步狀態爲 0 的時候是無鎖狀態。若是當前線程不是獲取鎖的線程,直接返回 false。

acquire 方法會先查看同步狀態是否獲取成功,若是成功則方法結束返回,也就是 !tryAcquire == false ,若失敗則先調用 addWaiter 方法再調用 acquireQueued 方法

而後看一下第二條路線 addWaiter

這裏首先把當前線程和 Node 的節點類型進行封裝,Node 節點的類型有兩種,EXCLUSIVESHARED ,前者爲獨佔模式,後者爲共享模式,具體的區別咱們會在 AQS 源碼討論,這裏讀者只須要知道便可。

首先會進行 tail 節點的判斷,有沒有尾節點,其實沒有頭節點也就至關於沒有尾節點,若是有尾節點,就會原子性的將當前節點插入同步隊列中,再執行 enq 入隊操做,入隊操做至關於原子性的把節點插入隊列中。

若是當前同步隊列尾節點爲null,說明當前線程是第一個加入同步隊列進行等待的線程。

**在看第三條路線 acquireQueued **

主要會有兩個分支判斷,首先會進行無限循環中,循環中每次都會判斷給定當前節點的先驅節點,若是沒有先驅節點會直接拋出空指針異常,直到返回 true。

而後判斷給定節點的先驅節點是否是頭節點,而且當前節點可否獲取獨佔式鎖,若是是頭節點而且成功獲取獨佔鎖後,隊列頭指針用指向當前節點,而後釋放前驅節點。若是沒有獲取到獨佔鎖,就會進入 shouldParkAfterFailedAcquireparkAndCheckInterrupt 方法中,咱們貼出這兩個方法的源碼

shouldParkAfterFailedAcquire 方法主要邏輯是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS將節點狀態由 INITIAL 設置成 SIGNAL,表示當前線程阻塞。當 compareAndSetWaitStatus 設置失敗則說明 shouldParkAfterFailedAcquire 方法返回 false,而後會在 acquireQueued 方法中死循環中會繼續重試,直至compareAndSetWaitStatus 設置節點狀態位爲 SIGNAL 時 shouldParkAfterFailedAcquire 返回 true 時纔會執行方法 parkAndCheckInterrupt 方法。(這塊在後面研究 AQS 會細講)

parkAndCheckInterrupt 該方法的關鍵是會調用 LookSupport.park 方法(關於LookSupport會在之後的文章進行討論),該方法是用來阻塞當前線程。

因此 acquireQueued 主要作了兩件事情:若是當前節點的前驅節點是頭節點,而且可以獲取獨佔鎖,那麼當前線程可以得到鎖該方法執行結束退出

若是獲取鎖失敗的話,先將節點狀態設置成 SIGNAL,而後調用 LookSupport.park 方法使得當前線程阻塞。

若是 !tryAcquireacquireQueued 都爲 true 的話,則打斷當前線程。

那麼它們的主要流程以下(注:只是加鎖流程,並非 lock 全部流程)

非公平鎖的加鎖(lock)流程詳解

非公平鎖的加鎖步驟和公平鎖的步驟只有兩處不一樣,一處是非公平鎖在加鎖前會直接使用 CAS 操做設置同步狀態,若是設置成功,就會把當前線程設置爲偏向鎖的線程;一處是 CAS 操做失敗執行 tryAcquire 方法,讀取線程同步狀態,若是未加鎖會使用 CAS 再次進行加鎖,不會等待 hasQueuedPredecessors 方法的執行,達到只要線程釋放鎖就會加鎖的目的。下面經過源碼和流程圖來詳細理解

這是非公平鎖和公平鎖不一樣的兩處地方,下面是非公平鎖的加鎖流程圖

lockInterruptibly 以可中斷的方式獲取鎖

如下是 JavaDoc 官方解釋:

lockInterruptibly 的中文意思爲若是沒有被打斷,則獲取鎖。若是沒有其餘線程持有該鎖,則獲取該鎖並當即返回,將鎖保持計數設置爲1。若是當前線程已經持有鎖,那麼此方法會馬上返回而且持有鎖的數量會 + 1。若是鎖是由另外一個線程持有的,則出於線程調度目的,當前線程將被禁用,並處於休眠狀態,直到發生如下兩種狀況之一

  • 鎖被當前線程持有
  • 一些其餘線程打斷了當前線程

若是當前線程獲取了鎖,則鎖保持計數將設置爲1。

若是當前線程發生了以下狀況:

  • 在進入此方法時設置了其中斷狀態
  • 當獲取鎖的時候發生了中斷(Thread.interrupt)

那麼當前線程就會拋出InterruptedException 而且當前線程的中斷狀態會清除。

下面看一下它的源碼是怎麼寫的

首先會調用 acquireInterruptibly 這個方法,判斷當前線程是否被中斷,若是中斷拋出異常,沒有中斷則判斷公平鎖/非公平鎖 是否已經獲取鎖,若是沒有獲取鎖(tryAcquire 返回 false)則調用 doAcquireInterruptibly 方法,這個方法和 acquireQueued 方法沒什麼區別,就是線程在等待狀態的過程當中,若是線程被中斷,線程會拋出異常。

下面是它的流程圖

tryLock 嘗試加鎖

僅僅當其餘線程沒有獲取這把鎖的時候獲取這把鎖,tryLock 的源代碼和非公平鎖的加鎖流程基本一致,它的源代碼以下

tryLock 超時獲取鎖

ReentrantLock除了能以中斷的方式去獲取鎖,還能夠以超時等待的方式去獲取鎖,所謂超時等待就是線程若是在超時時間內沒有獲取到鎖,那麼就會返回false,而不是一直死循環獲取。可使用 tryLock 和 tryLock(timeout, unit)) 結合起來實現公平鎖,像這樣

if (lock.tryLock() || lock.tryLock(timeout, unit)) {...}
複製代碼

若是超過了指定時間,則返回值爲 false。若是時間小於或者等於零,則該方法根本不會等待。

它的源碼以下

首先須要瞭解一下 TimeUnit 工具類,TimeUnit 表示給定粒度單位的持續時間,而且提供了一些用於時分秒跨單位轉換的方法,經過使用這些方法進行定時和延遲操做。

toNanos 用於把 long 型表示的時間轉換成爲納秒,而後判斷線程是否被打斷,若是沒有打斷,則以公平鎖/非公平鎖 的方式獲取鎖,若是可以獲取返回true,獲取失敗則調用doAcquireNanos方法使用超時等待的方式獲取鎖。在超時等待獲取鎖的過程當中,若是等待時間大於應等待時間,或者應等待時間設置不合理的話,返回 false。

這裏面以超時的方式獲取鎖也能夠畫一張流程圖以下

unlock 解鎖流程

unlocklock 是一對情侶,它們分不開彼此,在調用 lock 後必須經過 unlock 進行解鎖。若是當前線程持有鎖,在調用 unlock 後,count 計數將減小。若是保持計數爲0就會進行解鎖。若是當前線程沒有持有鎖,在調用 unlock 會拋出 IllegalMonitorStateException 異常。下面是它的源碼

在有了上面閱讀源碼的經歷後,相信你會很快明白這段代碼的意思,鎖的釋放不會區分公平鎖仍是非公平鎖,主要的判斷邏輯就是 tryRelease 方法,getState 方法會取得同步鎖的重入次數,若是是獲取了偏向鎖,那麼可能會屢次獲取,state 的值會大於 1,這時候 c 的值 > 0 ,返回 false,解鎖失敗。若是 state = 1,那麼 c = 0,再判斷當前線程是不是獨佔鎖的線程,釋放獨佔鎖,返回 true,當 head 指向的頭結點不爲 null,而且該節點的狀態值不爲0的話纔會執行 unparkSuccessor 方法,再進行鎖的獲取。

ReentrantLock 其餘方法

isHeldByCurrentThread & getHoldCount

在多線程同時訪問時,ReentrantLock 由最後一次成功鎖定的線程擁有,當這把鎖沒有被其餘線程擁有時,線程調用 lock() 方法會馬上返回併成功獲取鎖。若是當前線程已經擁有鎖,這個方法會馬上返回。能夠經過 isHeldByCurrentThreadgetHoldCount 來進行檢查。

首先來看 isHeldByCurrentThread 方法

public boolean isHeldByCurrentThread() {
  return sync.isHeldExclusively();
}
複製代碼

根據方法名能夠略知一二,是否被當前線程持有,它用來詢問鎖是否被其餘線程擁有,這個方法和 Thread.holdsLock(Object) 方法內置的監視器鎖相同,而 Thread.holdsLock(Object) 是 Thread 類的靜態方法,是一個 native 類,它表示的意思是若是當前線程在某個對象上持有 monitor lock(監視器鎖) 就會返回 true。這個類沒有實際做用,僅僅用來測試和調試所用。例如

private ReentrantLock lock = new ReentrantLock();

public void lock(){
  assert lock.isHeldByCurrentThread();
}
複製代碼

這個方法也能夠確保重入鎖可以表現出不可重入的行爲

private ReentrantLock lock = new ReentrantLock();

public void lock(){
  assert !lock.isHeldByCurrentThread();
  lock.lock();
  try {
    // 執行業務代碼
  }finally {
    lock.unlock();
  }
}
複製代碼

若是當前線程持有鎖則 lock.isHeldByCurrentThread() 返回 true,不然返回 false。

咱們在瞭解它的用法後,看一下它內部是怎樣實現的,它內部只是調用了一下 sync.isHeldExclusively(),sync 是 ReentrantLock 的一個靜態內部類,基於 AQS 實現,而 AQS 它是一種抽象隊列同步器,是許多併發實現類的基礎,例如 ReentrantLock/Semaphore/CountDownLatch。sync.isHeldExclusively() 方法以下

protected final boolean isHeldExclusively() {
  return getExclusiveOwnerThread() == Thread.currentThread();
}
複製代碼

此方法會在擁有鎖以前先去讀一下狀態,若是當前線程是鎖的擁有者,則不須要檢查。

getHoldCount()方法和isHeldByCurrentThread 都是用來檢查線程是否持有鎖的方法,不一樣之處在於 getHoldCount() 用來查詢當前線程持有鎖的數量,對於每一個未經過解鎖操做匹配的鎖定操做,線程都會保持鎖定狀態,這個方法也一般用於調試和測試,例如

private ReentrantLock lock = new ReentrantLock();

public void lock(){
  assert lock.getHoldCount() == 0;
  lock.lock();
  try {
    // 執行業務代碼
  }finally {
    lock.unlock();
  }
}
複製代碼

這個方法會返回當前線程持有鎖的次數,若是當前線程沒有持有鎖,則返回0。

newCondition 建立 ConditionObject 對象

ReentrantLock 能夠經過 newCondition 方法建立 ConditionObject 對象,而 ConditionObject 實現了 Condition 接口,關於 Condition 的用法咱們後面再講。

isLocked 判斷是否鎖定

查詢是否有任意線程已經獲取鎖,這個方法用來監視系統狀態,而不是用來同步控制,很簡單,直接判斷 state 是否等於0。

isFair 判斷是不是公平鎖的實例

這個方法也比較簡單,直接使用 instanceof 判斷是否是 FairSync 內部類的實例

public final boolean isFair() {
  return sync instanceof FairSync;
}
複製代碼

getOwner 判斷鎖擁有者

判斷同步狀態是否爲0,若是是0,則沒有線程擁有鎖,若是不是0,直接返回獲取鎖的線程。

final Thread getOwner() {
  return getState() == 0 ? null : getExclusiveOwnerThread();
}
複製代碼

hasQueuedThreads 是否有等待線程

判斷是否有線程正在等待獲取鎖,若是頭節點與尾節點不相等,說明有等待獲取鎖的線程。

public final boolean hasQueuedThreads() {
  return head != tail;
}
複製代碼

isQueued 判斷線程是否排隊

判斷給定的線程是否正在排隊,若是正在排隊,返回 true。這個方法會遍歷隊列,若是找到匹配的線程,返回true

public final boolean isQueued(Thread thread) {
  if (thread == null)
    throw new NullPointerException();
  for (Node p = tail; p != null; p = p.prev)
    if (p.thread == thread)
      return true;
  return false;
}
複製代碼

getQueueLength 獲取隊列長度

此方法會返回一個隊列長度的估計值,該值只是一個估計值,由於在此方法遍歷內部數據結構時,線程數可能會動態變化。 此方法設計用於監視系統狀態,而不用於同步控制。

public final int getQueueLength() {
  int n = 0;
  for (Node p = tail; p != null; p = p.prev) {
    if (p.thread != null)
      ++n;
  }
  return n;
}
複製代碼

getQueuedThreads 獲取排隊線程

返回一個包含可能正在等待獲取此鎖的線程的集合。 由於實際的線程集在構造此結果時可能會動態更改,因此返回的集合只是一個大概的列表集合。 返回的集合的元素沒有特定的順序。

public final Collection<Thread> getQueuedThreads() {
  ArrayList<Thread> list = new ArrayList<Thread>();
  for (Node p = tail; p != null; p = p.prev) {
    Thread t = p.thread;
    if (t != null)
      list.add(t);
  }
  return list;
}
複製代碼

回答上面那個問題

那麼你看完源碼分析後,你能總結出 synchronizedlock 鎖的實現 ReentrantLock 有什麼異同嗎?

Synchronzied 和 Lock 的主要區別以下:

  • 存在層面:Syncronized 是Java 中的一個關鍵字,存在於 JVM 層面,Lock 是 Java 中的一個接口

  • 鎖的釋放條件:1. 獲取鎖的線程執行完同步代碼後,自動釋放;2. 線程發生異常時,JVM會讓線程釋放鎖;Lock 必須在 finally 關鍵字中釋放鎖,否則容易形成線程死鎖

  • 鎖的獲取: 在 Syncronized 中,假設線程 A 得到鎖,B 線程等待。若是 A 發生阻塞,那麼 B 會一直等待。在 Lock 中,會分狀況而定,Lock 中有嘗試獲取鎖的方法,若是嘗試獲取到鎖,則不用一直等待

  • 鎖的狀態:Synchronized 沒法判斷鎖的狀態,Lock 則能夠判斷

  • 鎖的類型:Synchronized 是可重入,不可中斷,非公平鎖;Lock 鎖則是 可重入,可判斷,可公平鎖

  • 鎖的性能:Synchronized 適用於少許同步的狀況下,性能開銷比較大。Lock 鎖適用於大量同步階段:

    Lock 鎖能夠提升多個線程進行讀的效率(使用 readWriteLock)

  • 在競爭不是很激烈的狀況下,Synchronized的性能要優於ReetrantLock,可是在資源競爭很激烈的狀況下,Synchronized的性能會降低幾十倍,可是ReetrantLock的性能能維持常態;

  • ReetrantLock 提供了多樣化的同步,好比有時間限制的同步,能夠被Interrupt的同步(synchronized的同步是不能Interrupt的)等

還有什麼要說的嗎

面試官可能還會問你 ReentrantLock 的加鎖流程是怎樣的,其實若是你能把源碼給他講出來的話,必定是高分。若是你記不住源碼流程的話能夠記住下面這個簡化版的加鎖流程

  • 若是 lock 加鎖設置成功,設置當前線程爲獨佔鎖的線程;

  • 若是 lock 加鎖設置失敗,還會再嘗試獲取一次鎖數量,

    若是鎖數量爲0,再基於 CAS 嘗試將 state(鎖數量)從0設置爲1一次,若是設置成功,設置當前線程爲獨佔鎖的線程;

    若是鎖數量不爲0或者上邊的嘗試又失敗了,查看當前線程是否是已是獨佔鎖的線程了,若是是,則將當前的鎖數量+1;若是不是,則將該線程封裝在一個Node內,並加入到等待隊列中去。等待被其前一個線程節點喚醒。

文章參考:

【試驗局】ReentrantLock中非公平鎖與公平鎖的性能測試

第五章 ReentrantLock源碼解析1--得到非公平鎖與公平鎖lock()

juejin.im/post/5c95df…

【JUC】JDK1.8源碼分析之ReentrantLock(三)

www.lagou.com/lgeduarticl…

相關文章
相關標籤/搜索