現在的位置: 首頁 > 綜合 > 正文

ReentrantLock是什麼?基本方法有哪些

2020年02月21日 綜合 ⁄ 共 8416字 ⁄ 字型大小 評論關閉

  在開始本篇文章的內容講述前,先來回答我一個問題,為什麼 JDK 提供一個 synchronized 關鍵字之後還要提供一個 Lock 鎖,這不是多此一舉嗎?難道 JDK 設計人員都是沙雕嗎?

  初識 ReentrantLockReentrantLock 位於 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 節點的類型有兩種,EXCLUSIVE和 SHARED ,前者為獨佔模式,後者為共享模式,具體的區別我們會在 AQS 源碼討論,這裡讀者只需要知道即可。

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

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

  在看第三條路線 acquireQueued

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

  然後判斷給定節點的先驅節點是不是頭節點,並且當前節點能否獲取獨佔式鎖,如果是頭節點並且成功獲取獨佔鎖後,隊列頭指針用指向當前節點,然後釋放前驅節點。如果沒有獲取到獨佔鎖,就會進入 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 方法中,我們貼出這兩個方法的源碼

  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 方法使得當前線程阻塞。

  如果 !tryAcquire 和 acquireQueued 都為 true 的話,則打斷當前線程。

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

  非公平鎖的加鎖步驟和公平鎖的步驟只有兩處不同,一處是非公平鎖在加鎖前會直接使用 CAS 操作設置同步狀態,如果設置成功,就會把當前線程設置為偏向鎖的線程;一處是 CAS 操作失敗執行 tryAcquire 方法,讀取線程同步狀態,如果未加鎖會使用 CAS 再次進行加鎖,不會等待 hasQueuedPredecessors 方法的執行,達到只要線程釋放鎖就會加鎖的目的。

  lockInterruptibly 以可中斷的方式獲取鎖

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

  那麼當前線程就會拋出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 解鎖流程

  unlock 和 lock 是一對情侶,它們分不開彼此,在調用 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() 方法會立刻返回並成功獲取鎖。如果當前線程已經擁有鎖,這個方法會立刻返回。可以通過 isHeldByCurrentThread 和 getHoldCount 來進行檢查。

  來看 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 getQueuedThreads() { ArrayList list = new ArrayList(); for (Node p = tail; p != null; p = p.prev) { Thread t = p.thread; if (t != null) list.add(t); } return list;}

  因此要熟練掌握ReentrantLock需要不斷嘗試和寫代碼的,這樣可以鞏固自己的基礎,要了解更多請關注學步園。

抱歉!評論已關閉.