【AQS面試篇】瞭解ReentrantLock嗎?講講其底層實現

iqqcode發表於2020-10-02

1. ReentrantLock和AQS的關係

Lock的使用:

Lock lock = new ReentrantLock();

// 加鎖
lock.lock();
try {
    // 執行緒任務
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 釋放鎖
    lock.unlock();
}

ReentrantLockReentrantReadWriteLock底層,是基於AQS來實現的

AQS(AbstractQueuedSynchronizer),抽象佇列同步器。ReentrantLock內部包含了AQS物件,AQS是ReentrantLock實現加鎖和釋放鎖的關鍵核心元件

在這裡插入圖片描述

AQS 佇列同步器 三要素 :

  1. CAS 通過此來競爭鎖資源
  2. LockSurpport.park/unpark 執行緒阻塞和釋放
  3. CLH 雙向連結串列 存放被阻塞的執行緒

AQS原理

AQS:AbstractQuenedSynchronizer抽象的佇列式同步器。是除了java自帶的synchronized關鍵字之外的鎖機制。AQS的全稱為(AbstractQueuedSynchronizer),這個類在java.util.concurrent.locks包

AQS的核心思想:

如果被請求的共享資源空閒,則將當前請求資源的執行緒設定為有效的工作執行緒,並將共享資源設定為鎖定狀態,如果被請求的共享資源被佔用,那麼就需要一套執行緒阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH佇列鎖實現的,即將暫時獲取不到鎖的執行緒加入到佇列中。

用大白話來說,AQS就是基於CLH佇列,用volatile修飾共享變數state,執行緒通過CAS去改變狀態符,成功則獲取鎖成功,失敗則進入等待佇列,等待被喚醒。

  • CLH(Craig,Landin,and Hagersten)佇列是一個虛擬的雙向佇列,虛擬的雙向佇列即不存在佇列例項,僅存在節點之間的關聯關係。

  • AQS是將每一條請求共享資源的執行緒封裝成一個CLH鎖佇列的一個結點(Node),來實現鎖的分配

在這裡插入圖片描述

AQS維護了一個volatile int state和一個FIFO執行緒等待佇列,多執行緒爭用資源被阻塞的時候就會進入這個佇列。

state就是共享資源,其訪問方式有如下三種:

  • getState()
  • setState()
  • compareAndSetState();

CLH佇列–雙向連結串列實現

?AQS 同步器原理分析

AQS Node節點被 volatile 修飾,head 和 tail(即:連結串列的頭部和尾部)

// Head of the wait queue, lazily initialized. 
private transient volatile Node head;
 
// Tail of the wait queue, lazily initialized. .
private transient volatile Node tail;

在這裡插入圖片描述

注意:AQS是自旋鎖: 在等待喚醒的時候,經常會使用自旋while(!cas())的方式,不停地嘗試獲取鎖,直到被其他執行緒獲取成功

實現了AQS的鎖有:自旋鎖、互斥鎖、讀鎖寫鎖、條件產量、訊號量、柵欄都是AQS的衍生物


2. ReentrantLock鎖機制原理

AQS內部核心

  • state int型別,初始為0。代表加鎖的狀態
  • 關鍵變數,記錄當前獲取到鎖的執行緒,初值為null

在這裡插入圖片描述

初始時,執行緒A呼叫RentrantLocklock方法嘗試加鎖,加鎖的過程是用CASstate由0變為1

此時執行緒A獲取鎖成功

執行緒A加鎖成功後,將加鎖執行緒設定為自己

在這裡插入圖片描述

可重入鎖

執行緒A再次獲取到了鎖,判斷當前加鎖執行緒是否是自己?,是的話再次獲取鎖成功,satte類加1

互斥鎖

執行緒B想要獲取鎖,先判斷state是否為0

  • state = 0,嘗試獲取鎖
  • state != 0,看持有鎖的執行緒是否是自己;不是則進入到等待佇列
  • 執行緒B等待執行緒A釋放鎖之後,嘗試重新獲取

在這裡插入圖片描述

AQS中有一個等待佇列,存放獲取鎖?失敗的執行緒

在這裡插入圖片描述

釋放鎖

state變數減一,為0時徹底釋放鎖,不再持有資源,直至 加鎖執行緒為null

接下來,從等待佇列的對頭喚醒執行緒B,重新嘗試加鎖

  • 執行緒B重複上述操作,獲取鎖成功

在這裡插入圖片描述

3. AQS資源共享方式

AQS 定義了兩種資源共享方式:

  1. Exclusive:獨佔,只有一個執行緒能執行,如ReentrantLock
  2. Share:共享,多個執行緒可以同時執行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier

不同的自定義的同步器爭用共享資源的方式也不同


4. AQS底層使用了模板方法模式

同步器的設計是基於模板方法模式的,如果需要自定義同步器一般的方式是這樣(模板方法模式很經典的一個應用):

  1. 使用者繼承AbstractQueuedSynchronizer並重寫指定的方法。
  2. 將AQS組合在自定義同步元件的實現中,並呼叫其模板方法,而這些模板方法會呼叫使用者重寫的方法。
    這和我們以往通過實現介面的方式有很大區別,這是模板方法模式很經典的一個運用。

自定義同步器在實現的時候只需要實現共享資源state的獲取和釋放方式即可,至於具體執行緒等待佇列的維護,AQS已經在頂層實現好了。自定義同步器實現的時候主要實現下面幾種方法:

  • isHeldExclusively():該執行緒是否正在獨佔資源。只有用到Condition才需要去實現它;

獨佔

  • tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false;
  • tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false;

共享

  • tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源;
  • tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回false;

ReentrantLock為例

(可重入獨佔式鎖):state初始化為0,表示未鎖定狀態,A執行緒lock()時,會呼叫tryAcquire()獨佔鎖並將state+1.之後其他執行緒再想tryAcquire的時候就會失敗,直到A執行緒unlock()到state=0為止,其他執行緒才有機會獲取該鎖。A釋放鎖之前,自己也是可以重複獲取此鎖(state累加),這就是可重入的概念。

注意:獲取多少次鎖就要釋放多少次鎖,保證state是能回到零態的

CountDownLatch為例

任務分N個子執行緒去執行,state就初始化 為N,N個執行緒並行執行,每個執行緒執行完之後countDown()一次,state就會CAS減一。當N子執行緒全部執行完畢,state=0,會unpark()主呼叫執行緒,主呼叫執行緒就會從await()函式返回,繼續之後的動作。

一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一種即可。但AQS也支援自定義同步器同時實現獨佔和共享兩種方式,如ReentrantReadWriteLock。



部分內容參考自:

相關文章