ReentrantLock核心原理,絕對(duì)干貨
前言
先來一個(gè)面試題:
面試官:我看你熟悉ReentrantLock源碼,能講講他的中斷鎖是怎么實(shí)現(xiàn)的么?
不知道也沒關(guān)系,看完這篇文章通過你的思考,能找到答案哦
那我們開始吧
ReentrantLock 中文我們叫做可重入互斥鎖,可重入的意思是同一個(gè)線程可以對(duì)同一個(gè)共享資源重復(fù)的加鎖或釋放鎖,互斥就是 AQS 中的排它鎖的意思,只允許一個(gè)線程獲得鎖。
簡(jiǎn)單應(yīng)用
ReentrantLock 的使用相比較 synchronized 會(huì)稍微繁瑣一點(diǎn),所謂顯示鎖,也就是你在代碼中需要主動(dòng)的去進(jìn)行 lock 操作。一般來講我們可以按照下面的方式使用 ReentrantLock
- Lock lock = new ReentrantLock();
- lock.lock();
- try {
- doSomething();
- }finally {
- lock.unlock();
- }
lock.lock () 就是在顯式的上鎖。上鎖后,下面的代碼塊一定要放到 try 中,并且要結(jié)合 finally 代碼塊調(diào)用lock.unlock ()來釋放鎖,否則一定 doSomething 方法中出現(xiàn)任何異常,這個(gè)鎖將永遠(yuǎn)不會(huì)被釋放掉。
公平鎖和非公平鎖
synchronized 是非公平鎖,也就是說每當(dāng)鎖匙放的時(shí)候,所有等待鎖的線程并不會(huì)按照排隊(duì)順去依次獲得鎖,而是會(huì)再次去爭(zhēng)搶鎖。ReentrantLock 相比較而言更為靈活,它能夠支持公平和非公平鎖兩種形式。只需要在聲明的時(shí)候傳入 true。
- Lock lock = new ReentrantLock(true);
而默認(rèn)的無參構(gòu)造方法則會(huì)創(chuàng)建非公平鎖。
tryLock方法
前面我們通過 lock.lock (); 來完成加鎖,此時(shí)加鎖操作是阻塞的,直到獲取鎖才會(huì)繼續(xù)向下進(jìn)行。ReentrantLock 其實(shí)還有更為靈活的枷鎖方式 tryLock。
tryLock 方法有兩個(gè)重載,第一個(gè)是無參數(shù)的 tryLock 方法,被調(diào)用后,該方法會(huì)立即返回獲取鎖的情況。獲取為 true,未能獲取為 false。我們的代碼中可以通過返回的結(jié)果進(jìn)行進(jìn)一步的處理。第二個(gè)是有參數(shù)的 tryLock 方法,通過傳入時(shí)間和單位,來控制等待獲取鎖的時(shí)長(zhǎng)。如果超過時(shí)間未能獲取鎖則放回 false,反之返回 true。使用方法如下:
- if(lock.tryLock(2, TimeUnit.SECONDS)){
- try {
- doSomething();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- lock.unlock();
- }
- }else{
- doSomethingElse();
- }
我們?nèi)绻幌M麩o法獲取鎖時(shí)一直等待,而是希望能夠去做一些其它事情時(shí),可以選擇此方式。
類結(jié)構(gòu)
ReentrantLock 類本身是不繼承 AQS 的,實(shí)現(xiàn)了 Lock 接口,如下:
- public class ReentrantLock implements Lock, java.io.Serializable {}
Lock 接口定義了各種加鎖,釋放鎖的方法,接口有如下幾個(gè):
- // 獲得鎖方法,獲取不到鎖的線程會(huì)到同步隊(duì)列中阻塞排隊(duì)
- void lock();
- // 獲取可中斷的鎖
- void lockInterruptibly() throws InterruptedException;
- // 嘗試獲得鎖,如果鎖空閑,立馬返回 true,否則返回 false
- boolean tryLock();
- // 帶有超時(shí)等待時(shí)間的鎖,如果超時(shí)時(shí)間到了,仍然沒有獲得鎖,返回 false
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
- // 釋放鎖
- void unlock();
- // 得到新的 Condition
- Condition newCondition();
ReentrantLock 就負(fù)責(zé)實(shí)現(xiàn)這些接口,我們使用時(shí),直接面對(duì)的也是這些方法,這些方法的底層實(shí)現(xiàn)都是交給 Sync 內(nèi)部類去實(shí)現(xiàn)的,Sync 類的定義如下:
- abstract static class Sync extends AbstractQueuedSynchronizer {}
都是最終繼承自 AbstractQueuedSynchronizer。這就是著名的 AQS。通過查看 AQS 的注釋我們了解到, AQS 依賴先進(jìn)先出隊(duì)列實(shí)現(xiàn)了阻塞鎖和相關(guān)的同步器(信號(hào)量、事件等)。
AQS 內(nèi)部有一個(gè) volatile 類型的 state 屬性,實(shí)際上多線程對(duì)鎖的競(jìng)爭(zhēng)體現(xiàn)在對(duì) state 值寫入的競(jìng)爭(zhēng)。一旦 state 從 0 變?yōu)? 1,代表有線程已經(jīng)競(jìng)爭(zhēng)到鎖,那么其它線程則進(jìn)入等待隊(duì)列。
等待隊(duì)列是一個(gè)鏈表結(jié)構(gòu)的 FIFO 隊(duì)列,這能夠確保公平鎖的實(shí)現(xiàn)。同一線程多次獲取鎖時(shí),如果之前該線程已經(jīng)持有鎖,那么對(duì) state 再次加 1。釋放鎖時(shí),則會(huì)對(duì) state-1。直到減為 0,才意味著此線程真正釋放了鎖。
Sync 繼承了 AbstractQueuedSynchronizer ,所以 Sync 就具有了鎖的框架,根據(jù) AQS 的框架,Sync 只需要實(shí)現(xiàn) AQS 預(yù)留的幾個(gè)方法即可,但 Sync 也只是實(shí)現(xiàn)了部分方法,還有一些交給子類 NonfairSync 和 FairSync 去實(shí)現(xiàn)了,NonfairSync 是非公平鎖,F(xiàn)airSync 是公平鎖,定義如下:
- // 同步器 Sync 的兩個(gè)子類鎖
- static final class FairSync extends Sync {}
- static final class NonfairSync extends Sync {}
幾個(gè)類整體的結(jié)構(gòu)如下:
圖中 Sync、NonfairSync、FairSync 都是靜態(tài)內(nèi)部類的方式實(shí)現(xiàn)的,這個(gè)也符合 AQS 框架定義的實(shí)現(xiàn)標(biāo)準(zhǔn)。
構(gòu)造器
ReentrantLock 構(gòu)造器有兩種,代碼如下:
- // 無參數(shù)構(gòu)造器,相當(dāng)于 ReentrantLock(false),默認(rèn)是非公平的
- public ReentrantLock() {
- sync = new NonfairSync();
- }
- public ReentrantLock(boolean fair) {
- sync = fair ? new FairSync() : new NonfairSync();
- }
無參構(gòu)造器默認(rèn)構(gòu)造是非公平的鎖,有參構(gòu)造器可以選擇。
從構(gòu)造器中可以看出,公平鎖是依靠 FairSync 實(shí)現(xiàn)的,非公平鎖是依靠 NonfairSync 實(shí)現(xiàn)的
源碼解析
Sync同步器
Sync 表示同步器,繼承了 AQS:
從圖中可以看出,lock 方法是個(gè)抽象方法,留給 FairSync 和 NonfairSync 兩個(gè)子類去實(shí)現(xiàn)。
加鎖方法
FairSync公平鎖
FairSync 公平鎖只實(shí)現(xiàn)了 lock 和 tryAcquire 兩個(gè)方法,lock 方法非常簡(jiǎn)單,如下:
- // acquire 是 AQS 的方法,表示先嘗試獲得鎖,失敗之后進(jìn)入同步隊(duì)列阻塞等待
- final void lock() {
- acquire(1);
- }
在 FairSync 并沒有重寫 acquire 方法代碼。調(diào)用的為 AbstractQueuedSynchronizer 的代碼,如下:
- public final void acquire(int arg) {
- if (!tryAcquire(arg) &&
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
- selfInterrupt();
- }
首先調(diào)用一次 tryAcquire 方法。如果 tryAcquire 方法返回 true,那么 acquire 就會(huì)立即返回。
但如果 tryAcquire 返回了 false,那么則會(huì)先調(diào)用 addWaiter,把當(dāng)前線程包裝成一個(gè)等待的 node,加入到等待隊(duì)列。然后調(diào)用 acquireQueued 嘗試排隊(duì)獲取鎖,如果成功后發(fā)現(xiàn)自己被中斷過,那么返回 true,導(dǎo)致 selfInterrupt 被觸發(fā),這個(gè)方里只是調(diào)用Thread.currentThread ().interrupt ();進(jìn)行 interrupt。
acquireQueued 代碼如下:
在此方法中進(jìn)入自旋,不斷查看自己排隊(duì)的情況。如果輪到自己( header 是已經(jīng)獲取鎖的線程,而 header 后面的線程是排隊(duì)到要去獲取鎖的線程),那么調(diào)用 tryAcquire 方法去獲取鎖,然后把自己設(shè)置為隊(duì)列的 header。在自旋中,如果沒有排隊(duì)到自己,還會(huì)檢查是否應(yīng)該應(yīng)該被中斷。
整個(gè)獲取鎖的過程我們可以總結(jié)下:
直接通過 tryAcquire 嘗試獲取鎖,成功直接返回;
如果沒能獲取成功,那么把自己加入等待隊(duì)列;
自旋查看自己的排隊(duì)情況;
如果排隊(duì)輪到自己,那么嘗試通過 tryAcquire 獲取鎖;
如果沒輪到自己,那么回到第三步查看自己的排隊(duì)情況。
從以上過程我們可以看到鎖的獲取是通過 tryAcquire 方法。而這個(gè)方法在 FairSync 和 NonfairSync 有不同實(shí)現(xiàn)。
這個(gè)tryAcquire 方法是 AQS 在 acquire 方法中留給子類實(shí)現(xiàn)的抽象方法,F(xiàn)airSync 中實(shí)現(xiàn)的源碼如下:
實(shí)際上它的實(shí)現(xiàn)和 NonfairSync 的實(shí)現(xiàn),只是在 c==0 時(shí),多了對(duì) hasQueuedPredecessors 方法的調(diào)用。故名思義,這個(gè)方法做的事情就是判斷當(dāng)前線程是否前面還有排隊(duì)的線程。
當(dāng)它前面沒有排隊(duì)線程,說明已經(jīng)排隊(duì)到自己了,這是才會(huì)通過 CAS 的的方式去改變 state 值為 1,如果成功,那么說明當(dāng)前線程獲取鎖成功。接下來就是調(diào)用 setExclusiveOwnerThread 把自己設(shè)置成為鎖的擁有者。else if 中邏輯則是在處理重入邏輯,如果當(dāng)前線程就是鎖的擁有者,那么會(huì)把 state 加 1 更新回去。
通過以上分析,我們可以看出 AbstractQueuedSynchronizer 提供 acquire 方法的模板邏輯,但其中真正對(duì)鎖的獲取方法 tryAcquire,是在不同子類中實(shí)現(xiàn)的,這是很好的設(shè)計(jì)思想。
NonfairSync非公平鎖
NonfairSync 底層實(shí)現(xiàn)了 lock 和 tryAcquire 兩個(gè)方法,如下:
nonfairTryAcquire
以上代碼有三點(diǎn)需要注意:
通過判斷 AQS 的 state 的狀態(tài)來決定是否可以獲得鎖,0 表示鎖是空閑的;
else if 的代碼體現(xiàn)了可重入加鎖,同一個(gè)線程對(duì)共享資源重入加鎖,底層實(shí)現(xiàn)就是把 state + 1,并且可重入的次數(shù)是有限制的,為 Integer 的最大值;
這個(gè)方法是非公平的,所以只有非公平鎖才會(huì)用到,公平鎖是另外的實(shí)現(xiàn)。
無參的 tryLock 方法調(diào)用的就是此方法,tryLock 的方法源碼如下:
- public boolean tryLock() {
- // 入?yún)?shù)是 1 表示嘗試獲得一次鎖
- return sync.nonfairTryAcquire(1);
- }
其底層的調(diào)用關(guān)系(只是簡(jiǎn)單表明調(diào)用關(guān)系,并不是完整分支圖)如下:
解鎖方法
- public void unlock() {
- sync.release(1);
- }
和 lock 很像,實(shí)際調(diào)用的是 sync 實(shí)現(xiàn)類的 release 方法。和 lock 方法一樣,這個(gè) release 方法在 AbstractQueuedSynchronizer 中,
- if (tryRelease(arg)) {
- Node h = head;
- if (h != null && h.waitStatus != 0)
- unparkSuccessor(h);
- return true;
- }
- return false;
這個(gè)方法中會(huì)先執(zhí)行 tryRelease,它的實(shí)現(xiàn)也在 AbstractQueuedSynchronizer 的子類 Sync 中,如果釋放鎖成功,那么則會(huì)通過 unparkSuccessor 方法找到隊(duì)列中第一個(gè) waitStatus<0的線程進(jìn)行喚醒。我們下面看一下 tryRelease 方法代碼:
tryRelease 方法是公平鎖和非公平鎖都公用的,在鎖釋放的時(shí)候,是沒有公平和非公平的說法的。
從代碼中可以看到,鎖最終被釋放的標(biāo)椎是 state 的狀態(tài)為 0,在重入加鎖的情況下,需要重入解鎖相應(yīng)的次數(shù)后,才能最終把鎖釋放,比如線程 A 對(duì)共享資源 B 重入加鎖 5 次,那么釋放鎖的話,也需要釋放 5 次之后,才算真正的釋放該共享資源了。
總結(jié)
本篇文章 ReentrantLock 的使用及其核心源代碼,其實(shí) Lock 相關(guān)的代碼還有很多。我們可以嘗試自己去閱讀。
ReentrantLock 的設(shè)計(jì)思想是通過 FIFO 的隊(duì)列保存等待鎖的線程。通過 volatile 類型的 state 保存鎖的持有數(shù)量,從而實(shí)現(xiàn)了鎖的可重入性。而公平鎖則是通過判斷自己是否排隊(duì)成功,來決定是否去爭(zhēng)搶鎖。
然后我們了解到AQS 搭建了整個(gè)鎖架構(gòu),子類鎖只需要根據(jù)場(chǎng)景,實(shí)現(xiàn) AQS 對(duì)應(yīng)的方法即可,不僅僅是 ReentrantLock 是這樣,JUC 中的其它鎖也都是這樣,只要對(duì) AQS 了如指掌,鎖其實(shí)非常簡(jiǎn)單。
本文轉(zhuǎn)載自微信公眾號(hào)「月伴飛魚」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系月伴飛魚公眾號(hào)。