給讀者的小驚喜:戳我
前言
鎖是一種用來控制多線程訪問共享資源的工具。通常,鎖可以獨(dú)占共享資源:同一時間只有一個線程可以獲得鎖,并且所有訪問共享資源的線程都必須首先獲得鎖。前面我們介紹過了synchronized,使用synchronized的方法和代碼塊作用域機(jī)制使得使用監(jiān)視器鎖更加簡單,并且?guī)椭苊饬嗽S多關(guān)于鎖的常見編程錯誤,比如鎖未及時釋放等問題。但是有時候我們需要更靈活的使用鎖資源,例如,一些遍歷并發(fā)訪問的數(shù)據(jù)結(jié)構(gòu)的算法需要使用“手動”方法,或者“鎖鏈”:你先獲得節(jié)點(diǎn)A的鎖,然后是節(jié)點(diǎn)B,然后釋放A獲得C,再釋放B獲得D,以此類推。這種方式如果要使用synchronized就不是很好實(shí)現(xiàn),但是有了Lock就不一樣了,Lock接口允許以不同的范圍去獲取和釋放鎖,并且允許同時獲得多把鎖,也可以以任意的順序釋放。
Lock
Lock在J.U.C 中是最核心的組件,Lock是一個接口,它定義了釋放鎖和獲得鎖的抽象方法,今天我們要分析的ReentrantLock就實(shí)現(xiàn)了Lock接口,Lock接口中主要定義了5個方法:
| 方法 | 描述 |
|---|---|
| void lock() | 獲取鎖,該方法不會響應(yīng)中斷。如果獲取鎖失敗,當(dāng)前線程將被阻塞直到成功獲得鎖 |
| void lockInterruptibly() throws InterruptedException | 獲取鎖,會響應(yīng)中斷。其他和lock()方法一樣 |
| boolean tryLock() | 非阻塞獲取鎖,該方法需要立即返回獲取鎖結(jié)果,成功返回true,失敗返回false |
| boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 指定時間內(nèi)獲取鎖,會響應(yīng)中斷: 1.在指定的超時時間內(nèi)獲得鎖成功則返回true; 2.在超時時間內(nèi)被中斷則立刻返回獲得鎖結(jié)果; 3.在指定超時時間結(jié)束后立刻返回獲得鎖結(jié)果 |
| void unlock() | 釋放鎖。如果沒有獲得鎖則會拋異常 |
| Condition newCondition() |
初識ReentrantLock
ReentrantLock,重入鎖。表示支持重新進(jìn)入的鎖,也就是說,如果當(dāng)前線程 t1 通過調(diào)用 lock方法獲取了鎖之后,再次調(diào)用 lock,是不會再阻塞去獲取鎖的,直接增加重入次數(shù)就行了(synchronized也是支持重入的)。
ReentrantLock基本使用
ReentrantLock的使用非常簡單,下面就是一個使用的例子:
package com.zwx.concurrent;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock();
try {
System.out.println(1);
}finally {
lock.unlock();
}
}
}
使用Lock的時候尤其要注意,當(dāng)加鎖和解鎖發(fā)生在不同地方時,必須小心確保所有的持有鎖的代碼都受到try-finally或try-catch代碼塊的保護(hù),用以確保在必要時鎖得到釋放,這也是靈活使用所帶來的代價,因?yàn)槭褂胹ynchronized時,不需要考慮這個問題。
ReentrantLock原理
假如我們是Lock的設(shè)計(jì)者,那我們想一下,如果多個線程都來并發(fā)搶占同一把鎖資源時,而同一時間肯定只有一個線程能搶占成功,那么搶占失敗的線程怎么辦?答案很簡單,肯定是找個地方存起來,但是具體要怎么存,這個就是個值得思考的問題。ReentrantLock中采用的是一個同步隊(duì)列的數(shù)據(jù)結(jié)構(gòu)來存儲阻塞等待的線程。
同步隊(duì)列(AQS)
同步隊(duì)列,全稱為:AbstractQueuedSynchronizer,又可以簡稱為AQS。在Lock中,這是一個非常重要的核心組件,J.U.C工具包中很多地方都使用到了AQS,所以,如果理解了AQS,那么再去理解ReentrantLock,Condition,CountDownLatch等工具的實(shí)現(xiàn)原理,就會非常輕松。
AQS的兩種功能
從使用層面來說,AQS 的功能分為兩種:獨(dú)占和共享。
- 獨(dú)占鎖:每次只有一個線程持有鎖,如:ReentrantLock 就是以獨(dú)占方式實(shí)現(xiàn)的互斥鎖
- 共享鎖:允許多個線程同時獲取鎖 ,并發(fā)訪問共享資源 , 如:ReentrantReadWriteLock
AQS 的內(nèi)部實(shí)現(xiàn)
AQS依賴內(nèi)部的一個FIFO雙向隊(duì)列來完成同步狀態(tài)的管理,當(dāng)前線程獲取鎖失敗時,AQS會將當(dāng)前線程以及等待狀態(tài)等信息構(gòu)造成為一個節(jié)點(diǎn)(Node對象)并將其加入AQS中,同時會阻塞當(dāng)前線程,當(dāng)鎖被釋放時,會把首節(jié)點(diǎn)中的線程喚醒,使其再次嘗試獲取同步狀態(tài)。AQS中有一個頭(head)節(jié)點(diǎn)和一個尾(tail)節(jié)點(diǎn),中間每個節(jié)點(diǎn)(Node)都有一個prev和next指針指向前一個節(jié)點(diǎn)和后一個節(jié)點(diǎn),如下圖:

<figcaption></figcaption>
Node對象組成
AQS中每一個節(jié)點(diǎn)就時一個Node對象,并且通過節(jié)點(diǎn)中的狀態(tài)等信息來控制隊(duì)列,Node對象是AbstractQueuedSynchronizer對象中的一個靜態(tài)內(nèi)部類,下面就是Node對象的源碼:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;//表示當(dāng)前線程狀態(tài)是取消的
static final int SIGNAL = -1;//表示當(dāng)前線程正在等待鎖
static final int CONDITION = -2;//Condition隊(duì)列有使用到,暫時用不到
static final int PROPAGATE = -3;//CountDownLatch等工具中使用到,暫時用不到
volatile int waitStatus;//節(jié)點(diǎn)中線程的狀態(tài),默認(rèn)為0
volatile Node prev;//當(dāng)前節(jié)點(diǎn)的前一個節(jié)點(diǎn)
volatile Node next;//當(dāng)前節(jié)點(diǎn)的后一個節(jié)點(diǎn)
volatile Thread thread;//當(dāng)前節(jié)點(diǎn)封裝的線程信息
Node nextWaiter;//Condition隊(duì)列中的關(guān)系,暫時用不到
final boolean isShared() {//暫時用不到
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {//獲取當(dāng)前節(jié)點(diǎn)的上一個節(jié)點(diǎn)
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
Node(Thread thread, Node mode) {//構(gòu)造一個節(jié)點(diǎn):addWaiter方法中會使用,此時waitStatus默認(rèn)等于0
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { //構(gòu)造一個節(jié)點(diǎn):Condition中會使用
this.waitStatus = waitStatus;
this.thread = thread;
}
}
上面代碼中加上了注釋,總來來說應(yīng)該還是比較好理解,注意,Node對象并不是AQS才會使用,Condition隊(duì)列以及其他工具中也會使用,所以有些狀態(tài)和方法在這里是暫時用不上的本文就不會過多關(guān)注。
lock.lock()源碼解讀
上面花了一點(diǎn)篇幅介紹了ReentrantLock內(nèi)部的實(shí)現(xiàn)機(jī)制,相信大家的腦海里有了一個初步的輪廓,接下來就讓我們一步步從加鎖到釋放鎖進(jìn)行源碼解讀吧
ReentrantLock#lock()
上文的示例中,當(dāng)我們調(diào)用lock.lock()時,我們進(jìn)入Lock接口的實(shí)現(xiàn)類ReentrantLock中的加鎖入口:

然后發(fā)現(xiàn)這里調(diào)用了sync中的一個lock()方法,sync是ReentrantLock類當(dāng)中的一個組合類,我們知道,AQS是一個同步隊(duì)列,但是因?yàn)锳QS只是一個同步隊(duì)列,并不具備一些業(yè)務(wù)執(zhí)行能力,所以通過了另一個Sync來繼承AbstractQueuedSynchronizer,并根據(jù)不同的業(yè)務(wù)場景來實(shí)現(xiàn)不同的業(yè)務(wù)邏輯:

進(jìn)入Sync類,我們發(fā)現(xiàn),Sync的lock()方法是一個抽象方法,也就是說我們還需要去執(zhí)行Sync中的實(shí)現(xiàn)類,Sync有兩個實(shí)現(xiàn)類:FairSync和NonfairSync,也就是公平鎖和非公平鎖。
公平鎖和非公平鎖其實(shí)大致邏輯都是差不多,唯一的區(qū)別是非公平鎖在搶占鎖之前會先判斷一下當(dāng)前AQS隊(duì)列中是不是有線程正在等待,如果沒有才會通過CAS操作去搶占鎖,相信看完這篇文章,大家自己去看也會很容易理解。這里我們就以非公平鎖為例,進(jìn)入非公平鎖中的lock()方法繼續(xù)我們的源碼之旅:
NonfairSync#lock()
這個方法很簡短,首先通過一個CAS操作,如果CAS成功則表示當(dāng)前線程獲得鎖成功,獲取鎖成功之后,需要在AQS當(dāng)中記錄一下當(dāng)前獲得鎖的線程信息:
CAS操作
CAS操作是多線程當(dāng)中的一個原子操作。

上面代碼的意思是,如果當(dāng)前內(nèi)存中的stateOffset的值和預(yù)期值expect相等,則替換為 update值。更新成功返回true,否則返回false。這個操作是原子的,不會出現(xiàn)線程安全問題。
stateOffset是AQS中的一個屬性,它在不同的實(shí)現(xiàn)中所表達(dá)的含義不一樣,對于重入鎖的實(shí)現(xiàn)來說,表示一個同步狀態(tài)。它有兩個含義:
- 當(dāng) state=0 時,表示無鎖狀態(tài)
- 當(dāng) state>0 時,表示已經(jīng)有線程獲得了鎖,也就是state=1,但是因?yàn)镽eentrantLock允許重入,所以同一個線程多次獲得同步鎖的時候,state會遞增,比如重入 5 次,那么state=5。而在釋放鎖的時候,同樣需要釋放5次直到state=0其他線程才有資格獲得鎖。
至于Unsafe類中都是一些本地(native)方法,就不過多敘述了。
CAS操作的ABA問題
CAS操作是一個原子操作,但是CAS操作同樣會存在問題,這就是經(jīng)典的ABA問題。假如一個值一開始是A,然后被修改成B,最后又被改回了A,那么這時候有其他線程過來,會發(fā)現(xiàn)預(yù)期值還是是A,就會以為沒變(實(shí)際上變了:A-B-A),這時候就會CAS操作成功,解決這個問題的辦法就是可以加入一個版本號,比如原始值設(shè)置為A1(1表示版本號),改成B的時候版本號也同時遞增,修改成B2,這時候再次改回A,就變成A3,那么A1和A3就不相同了,可以避免了ABA問題的產(chǎn)生。
AQS#acquire(arg)
上面的操作中,我們假如線程A是第一次進(jìn)來,那么肯定獲得鎖成功,這時候我們可以得到這樣的一個AQS:
這時候AQS隊(duì)列還未構(gòu)造,僅僅只是設(shè)置了一些獨(dú)占線程相關(guān)的屬性。上圖中表示當(dāng)前對象已經(jīng)有線程ThreadA獲得鎖了,這時又來了個線程B,會去執(zhí)行CAS操作,因?yàn)榫€程A已經(jīng)獲得鎖了,狀態(tài)已經(jīng)等于1了,所以CAS失敗,這時候線程B就會調(diào)用AQS當(dāng)中的acquire方法(參數(shù)1是固定死的,表示加鎖次數(shù)):

這里也是一個if判斷,我們先看第一個條件,tryAcquire(arg)方法,這里我們同樣進(jìn)入非公平鎖實(shí)現(xiàn)NonfairSync類中,然后會發(fā)現(xiàn)最終其還是會調(diào)用父類Sync中的方法nonfairTryAcquire(arg)
Sync#nonfairTryAcquire(arg)
這個方法就是為了嘗試去獲得鎖,但是這里有一個問題,因?yàn)檫@個方法之所以會被執(zhí)行,就是因?yàn)榍懊娴腃AS操作失敗了,也就是獲得鎖失敗了,state就肯定不會為0了,可是這個方法的131行為什么還要再次判斷state是否等于0呢?
我們想一想,如果一個線程爭搶鎖失敗,我們應(yīng)該怎么做,無外乎兩個辦法:一個就是多試幾次,之前介紹synchronized的時候曾經(jīng)說過,大部分鎖被持有之后都會很快被釋放,所以再試試總沒有錯,萬一剛好鎖被釋放了呢。另一個辦法就是阻塞等待,這個后面會介紹,所以這里的131行代碼判斷也是這個邏輯,就是再試一次,如果成功,就可以直接獲得鎖,而不需要加入AQS隊(duì)列掛起線程了。
線程A沒有釋放鎖的時候,線程B會搶占鎖失敗,則返回false,我們回到之前的邏輯,會繼續(xù)執(zhí)行acquireQueued方法,這個方法里面有一個參數(shù)是addWaiter返回的,所以我們先去看addWaiter這個方法
AQS#addWaiter(Node)
走到這里就是說明當(dāng)前線程至少2次嘗試獲取鎖都失敗了,所以當(dāng)前線程會被初始化成為一個Node節(jié)點(diǎn),加入到AQS隊(duì)列中。我們前面提到了,AQS有兩種模式,一種獨(dú)占,一種共享,而重入鎖ReentrantLock是獨(dú)占的,所以這里固定傳入了參數(shù)Node.EXCLUSIVE表示當(dāng)前鎖是獨(dú)占的。而由前面Node對象的源碼可以知道,Node.EXCLUSIVE其實(shí)是一個null值的Node對象。
因?yàn)槲覀兊竭@里的時候是第一次進(jìn)來,AQS隊(duì)列還沒有被初始化,head和tail都是為空的,所以if判斷肯定不成立,也就是說,如果是第一次調(diào)用addWaiter方法時,會先執(zhí)行下面的enq(node)方法。
AQS#enq()
先不要看else邏輯,線程B第一次進(jìn)來肯定是走的if邏輯,初始化之后,得到這樣的一個AQS:
頭節(jié)點(diǎn)為什么放空線程
注意了,這里的頭節(jié)點(diǎn)中thread沒有賦值(thread=null),其實(shí)這里的第一個節(jié)點(diǎn)只是起了一個哨兵的作用,這樣就可以免去了后續(xù)在查找過程中每次比較是否越界的操作,后面會陸續(xù)提到這個哨兵的作用。
回到源碼邏輯來,因?yàn)樯厦媸且粋€死循環(huán),初始化之后,緊接著會立刻進(jìn)行第二次for循環(huán),第二次循環(huán)的時候tail節(jié)點(diǎn)不為空了,所以會走else邏輯,走完else邏輯之后會得到下面這樣一個AQS:
這時候假如又來了線程C,那么線程C就會走到AQS#addWaiter(Node)方法中上面的if邏輯了,因?yàn)檫@時候tail節(jié)點(diǎn)已經(jīng)不為空了,這里的if邏輯其實(shí)和enq(Node)方法中for循環(huán)中的else分支邏輯是一樣的,只是把線程C添加到AQS的尾部,最終會得到下面這個AQS:
接下來我們回到前面的方法,繼續(xù)執(zhí)行AQS中的acquireQueued(Node,arg)方法。
AQS#acquireQueued(Node,arg)
上面經(jīng)過addWaiter(Node)之后,阻塞的線程已經(jīng)被加入到了AQS隊(duì)列當(dāng)中,但是注意,這時候僅僅只是把線程加入進(jìn)去了,而線程并沒有被掛起,也就是說,線程還是處于運(yùn)行狀態(tài),那么接下來要做的事就是需要把加入AQS隊(duì)列中的線程掛起,當(dāng)然在掛起之前,還是我們前面說的,就是線程還是不死心,所以還需要最后搏一搏,萬一搶到鎖了,就不需要掛起了,所以這就是acquireQueued(Node,arg)方法中會做的兩件事:
1、看看前一個節(jié)點(diǎn)是不是頭節(jié)點(diǎn),如果是的話,就再試一次
2、再試一次如果還是失敗了,那么線程正式掛起
有幾個屬性這里可以先不管,關(guān)注for循環(huán)里面邏輯,首先獲取到前一個節(jié)點(diǎn),如果前一個節(jié)點(diǎn)是head節(jié)點(diǎn),那就再調(diào)用tryAcquire(arg)方法去搶一次鎖。
我們這里假設(shè)爭搶鎖還是失敗了,這時候就會走到882行的if判斷,if判斷中第一個邏輯看名字shouldParkAfterFailedAcquire能猜到大致意思,就是爭搶鎖失敗后看一下當(dāng)前線程是不是應(yīng)該掛起,我們進(jìn)入shouldParkAfterFailedAcquire方法看看:
上面這段代碼值得說的就是811-815行,我們先來演示下這個流程,因?yàn)橐瞥齝ancel狀態(tài)節(jié)點(diǎn)后面邏輯中還會出現(xiàn)。
1、假設(shè)ThreadB被取消了,那么這時候AQS中ThreadB節(jié)點(diǎn)狀態(tài)為-:
2、執(zhí)行813行代碼,相當(dāng)于:prev=prev.prev;node.prev=prev;得到如下AQS:
3、這時候while循環(huán)的條件肯定不成立,因?yàn)榇藭r的pred已經(jīng)指向了頭節(jié)點(diǎn),狀態(tài)為-1,
所以循環(huán)結(jié)束,繼續(xù)執(zhí)行815行代碼,得到如下AQS:
最終的結(jié)果我們可以看到,雖然ThreadB還有指向其他線程,但是我們通過其他任何節(jié)點(diǎn),都沒辦法找到ThreadB,已經(jīng)重新構(gòu)建了一個關(guān)聯(lián)關(guān)系,相當(dāng)于ThreadB被移出了隊(duì)列。
因?yàn)閔ead節(jié)點(diǎn)是一個哨兵,不可能會被取消,所以這里的while循環(huán)是不需要擔(dān)心pred會變?yōu)閚ull的。
暫時忘掉上面移除cancel節(jié)點(diǎn)的流程,我們假設(shè)是線程B進(jìn)來,那么前一個節(jié)點(diǎn)就是head節(jié)點(diǎn),肯定會走到最后一個else,這也是一個CAS操作,把頭節(jié)點(diǎn)狀態(tài)改為-1,如果是線程C進(jìn)來,就會把B節(jié)點(diǎn)設(shè)置為-1,這時候就會得到下面這樣一個AQS:
這個AQS隊(duì)列和上面的唯一區(qū)別就是前面兩個節(jié)點(diǎn)的waitStatus狀態(tài)從0改成了-1。
這里注意了,只有前一個節(jié)點(diǎn)waitStatus=-1才會返回true,所以這里第一次循環(huán)進(jìn)來肯定返回false,也就是還會再一次進(jìn)行循環(huán),循環(huán)的時候還會再次執(zhí)行上面的爭搶鎖方法(看起來真的是賊心不死哈)。判斷失敗后,就會二次進(jìn)入shouldParkAfterFailedAcquire方法,這時候因?yàn)榈谝淮窝h(huán)已經(jīng)把前一個節(jié)點(diǎn)狀態(tài)改為-1了,所以就會返回true了。
返回true之后,就會執(zhí)行if判斷的第二個邏輯了,這里面才是真的把線程正式掛起來。要掛起一個線程著實(shí)有點(diǎn)不容易哈哈。調(diào)用parkAndCheckInterrupt()方法正式掛起:

為什么要使用interrupted()返回中斷標(biāo)記
要解釋這個原因我們需要先解釋下park()方法:
LockSupport.park()方法是中斷一個線程,但是遇上下面三種情況,就會立即返回:
- 其他線程對當(dāng)前線程發(fā)起了unpark()操作時
- 其他線程中斷了當(dāng)前線程時
- 不合邏輯的調(diào)用(也就是沒有理由)時
第三點(diǎn)沒想明白場景,有知道的歡迎留言,感謝!
這里我們要說的是第2點(diǎn),其他線程中斷了當(dāng)前線程會有什么影響,我們先來演示一個例子再來得出結(jié)論:
當(dāng)park()遇上了interrupt()
前面講線程基本知識的時候,我們講到了sleep()遇到了interrupt()會怎么樣,感興趣的可以點(diǎn)擊這里詳細(xì)了解。
這里我們來看個例子:
package com.zwx.concurrent.lock;
import java.util.concurrent.locks.LockSupport;
public class LockParkInterrputDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
int i = 0;
while (true){
if(i == 0){
LockSupport.park();
//獲取中斷標(biāo)記,但是不復(fù)位
System.out.println(Thread.currentThread().isInterrupted());
LockSupport.park();
LockSupport.park();
System.out.println("如果走到這里就說明park不生效了");
}
i++;
if(i == Integer.MAX_VALUE){
break;
}
}
});
t1.start();
Thread.sleep(1000);//確保t1被park()之后再中斷
t1.interrupt();
System.out.println("end");
}
}
輸出結(jié)果:

所以其實(shí)park()方法至少有以下兩個個特點(diǎn):
- 當(dāng)一個線程park()時收到中斷信號,會立刻恢復(fù),且中斷標(biāo)記為true,而且不會拋出InterruptedException
- 當(dāng)一個線程中斷標(biāo)記為true時候,park()對其無效
有這兩個結(jié)論,上面就很好理解了,我們想一想,假設(shè)上面的線程掛起之后,并不是被線程A釋放鎖之后調(diào)用unpark()喚醒的,而是被其他線程中斷了,那么就會立刻恢復(fù)繼續(xù)后面的操作,這時候如果不對線程進(jìn)行復(fù)位,那么他會回到前面的死循環(huán),park()也無效了,就會一直死循環(huán)搶占鎖,會一直占用CPU資源,如果線程多了可能直接把CPU耗盡。
分析到這里,線程被掛起,告一段落。掛起之后需要等待線程A釋放鎖之后喚醒再繼續(xù)執(zhí)行。所以接下來我們看看unlock()是如何釋放鎖以及喚醒后續(xù)線程的。
lock.unlock()源碼解讀
ReentrantLock#unlock()
上文的示例中,當(dāng)我們調(diào)用lock.unlock()時,我們進(jìn)入Lock接口的實(shí)現(xiàn)類ReentrantLock中的釋放鎖入口:
這里和上文的加鎖不一樣,加鎖會區(qū)分公平鎖和非公平鎖,這里直接就是調(diào)用了sync父類AQS中的release(arg)方法:
我們可以看到,這里首先會調(diào)用tryRelease(arg)方法,最終會回到ReentrantLock類中的tryRelease(arg)方法:
ReentrantLock#tryRelease()
這個方法看起來就比較簡單了,釋放一次就把state-1,所以我們的lock()和unlock()是需要配對的,否則無法完全釋放鎖,這里因?yàn)槲覀儧]有重入,所以c=0,那么這時候的AQS隊(duì)列就變成了這樣:
當(dāng)前方法返回true,那么就會繼續(xù)執(zhí)行上面AQS#release(arg)方法中if里面的邏輯了:
這個方法就沒什么好說的,比較簡單了,我們直接進(jìn)入到unparkSuccessor(h)方法中一窺究竟。
AQS#unparkSuccessor(Node)
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
* 如果狀態(tài)是負(fù)的,嘗試去清除這個信號,當(dāng)然,如果清除失敗或者說被其他
* 等待獲取鎖的線程修改了,也沒關(guān)系。
* 這里為什么要去把狀態(tài)修改為0呢?其實(shí)這個線程是要被喚醒的,修不修改都無所謂。
* 回憶一下上面的acquireQueued方法中調(diào)用了shouldParkAfterFailedAcquire
* 去把前一個節(jié)點(diǎn)狀態(tài)改為-1,而在改之前會搶占一次鎖,所以說這里的操作
* 其實(shí)并沒有太大用處,可能可以為爭搶鎖的線程再多一次搶鎖機(jī)會,故而成功失敗均不影響
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
* 喚醒后繼節(jié)點(diǎn),通常是next節(jié)點(diǎn),但是如果next節(jié)點(diǎn)被取消了或者為空,那么
* 就需要從尾部開始遍歷,將無效節(jié)點(diǎn)先剔除
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {//如果下一個節(jié)點(diǎn)為空或者被取消了
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)//一直遍歷,直到找到狀態(tài)小于等于0的有效節(jié)點(diǎn)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
這段代碼中值得說明的是為什么要從tail節(jié)點(diǎn)開始循環(huán)遍歷。不知道大家對enq()方法中的構(gòu)造AQS隊(duì)列的步驟還有沒有印象,為了不讓大家翻上去找代碼,我把代碼重新貼下來:
我們看到,不管是if分支還是else分支,cas操作成功之后都只是把tail節(jié)點(diǎn)的關(guān)系構(gòu)造出來了,第一個if分支CAS操作后得到下面這樣的情況:
執(zhí)行else分支的CAS操作之后,可能得到下面這樣的情況:
我們可以發(fā)現(xiàn),上面兩種情況next節(jié)點(diǎn)都還沒來得及構(gòu)造,那么假如這時候從前面還是遍歷就會出現(xiàn)找不到節(jié)點(diǎn)的情況,但是從tail往前就不會有這個問題。
看到這里忍不住感嘆下,大佬的思維真是達(dá)到了一定的高度,寫的代碼完全都是精華。
到這里釋放鎖完成,下一個線程(ThreadB)也被喚醒了,那么下一個線程被喚醒后在哪里呢?還是把上面線程最終掛起的代碼貼出來:

也就是說線程被喚醒后,會繼續(xù)執(zhí)行return語句,返回中斷標(biāo)記。然后會回到AQS類中的
acquireQueued(Node,arg)方法
回到AQS#acquireQueued(Node,arg)
也就是說會回到上面代碼中的882行的if判斷,不管interrupted是等于true(想成掛起期間被中斷過)還是等于false,都不會跳出當(dāng)前的for循環(huán),那么就繼續(xù)循環(huán)。
因?yàn)楸粏拘训木€程是ThreadB,所以這時候if判斷成立,而且因?yàn)榇藭rstate=0,處于無鎖狀態(tài),tryAcquire(arg)獲取鎖也會成功,這時候AQS又變成了有鎖狀態(tài),只不過獨(dú)占線程由A變成了B:
這時候線程B獲取鎖成功了,所以必然要從AQS隊(duì)列中移除,我們進(jìn)入setHead(node)方法:
我們還是來演示一下這三行代碼:
1、head=node,于是得到如下AQS隊(duì)列:
2、node.Thread=null;node.prev=null;得到如下AQS隊(duì)列:
3、回到前一個方法,執(zhí)行setHead(Node)下一行代碼,p.next = null,得到如下最新的AQS:
經(jīng)過這三步,我們看到,原先的頭節(jié)點(diǎn)已經(jīng)沒有任何關(guān)聯(lián)關(guān)系了,其實(shí)在第二步的時候,原先頭節(jié)點(diǎn)已經(jīng)不在隊(duì)列中了,執(zhí)行第三步只是為了消除其持有的引用,方便被垃圾回收。
到這里,最終會執(zhí)行return interrupted;跳出循環(huán),繼續(xù)回到前一個方法。
回到AQS#acquire(arg)

這時候假如前面的interrupted返回true的話會執(zhí)行selfInterrupt()方法:

這里自己中斷自己的原因就是上面介紹過的,上面捕獲到線程中斷之后只是記錄下了中斷狀態(tài),然后對線程進(jìn)行了復(fù)位,所以這時候這里需要再次中斷自己,對外界做出響應(yīng)。
到這里,整個lock()和unlock()分析就結(jié)束了,但是上面acquireQueued方法我們這里需要再進(jìn)去看一下,里面的finally中有一個cancelAcquire(Node)方法。
AQS#cancelAcquire(Node)
private void cancelAcquire(Node node) {
if (node == null)//1
return;//2
node.thread = null;//3-將當(dāng)前節(jié)點(diǎn)的線程設(shè)置為null
// Skip cancelled predecessors 跳過已經(jīng)被取消的前置節(jié)點(diǎn)
Node pred = node.prev;//4
while (pred.waitStatus > 0)//5
node.prev = pred = pred.prev;//6
//predNext是很明顯需要解除關(guān)系的,如果不解除下面的cas操作將會失敗
Node predNext = pred.next;//7-如果上一個節(jié)點(diǎn)沒有不合法的,那么這個就是自己,否則就是當(dāng)前節(jié)點(diǎn)前面的某一個節(jié)點(diǎn)
node.waitStatus = Node.CANCELLED;//8
//1.如果當(dāng)前線程是tail節(jié)點(diǎn),直接移除掉,并且把上一個節(jié)點(diǎn)設(shè)置為tail節(jié)點(diǎn)
if (node == tail && compareAndSetTail(node, pred)) {//9
compareAndSetNext(pred, predNext, null);//10-這里要和上面Node predNext = pred.next結(jié)合起來理解
} else {//11
/**
* 如果下一個節(jié)點(diǎn)需要喚醒信號(即需要狀態(tài)設(shè)置為-1),嘗試把上一個節(jié)點(diǎn)的next節(jié)點(diǎn)設(shè)置
* 為當(dāng)前節(jié)點(diǎn)的下一個節(jié)點(diǎn),這樣他就可以得到一個喚醒的信號,如果設(shè)置信號失敗,那就直接喚醒
* 當(dāng)前節(jié)點(diǎn)的下一個節(jié)點(diǎn),并以此往后傳遞
*/
int ws;//12
//2.如果當(dāng)前線程前置節(jié)點(diǎn)是head節(jié)點(diǎn),且狀態(tài)為-1(不為-1但是設(shè)置為-1成功)
if (pred != head &&//13
((ws = pred.waitStatus) == Node.SIGNAL ||//14
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&//15
pred.thread != null) {//16
Node next = node.next;//17
if (next != null && next.waitStatus <= 0)//18
compareAndSetNext(pred, predNext, next);//19
} else {//20-當(dāng)前節(jié)點(diǎn)的前置節(jié)點(diǎn)是head節(jié)點(diǎn),那就直接把下一個節(jié)點(diǎn)喚醒
unparkSuccessor(node);//21-//這里面會去除狀態(tài)為cancel的節(jié)點(diǎn),而此時狀態(tài)已經(jīng)為-1了
}
node.next = node; //22-help GC
}
}
這個代碼邏輯上是有點(diǎn)繞的,所以還是要結(jié)合圖形來會比較好理解,而且這里有兩種情況,一種就是當(dāng)前隊(duì)列中沒有無效節(jié)點(diǎn)被清除,一種是有無效節(jié)點(diǎn)被清除,我們假設(shè)當(dāng)前有如下兩個隊(duì)列:
上圖中的AQS同步隊(duì)列中假設(shè)沒有無效節(jié)點(diǎn)需要被清除,這種場景的5和6行是可以忽略的,這時候第7行的predNext其實(shí)就是當(dāng)前節(jié)點(diǎn)自己。
假如這時候就是ThreadD進(jìn)來,而ThreadC是無效節(jié)點(diǎn),那么第5行和第6行就會執(zhí)行了,這時候predNext就是ThreadC所在的節(jié)點(diǎn)了,而不是ThreadD本身了,所以predNext在這種場景的時候就不會是自己了。
然后下面分了三種情況來進(jìn)行移除節(jié)點(diǎn)(為了便于理解,下圖中沒有將狀態(tài)改為-3頁也沒有將thread設(shè)置為null顯示出來):
-
當(dāng)前節(jié)點(diǎn)為tail節(jié)點(diǎn)(即ThreadD)
這種情況可以直接移除,所以第9行通過一個CAS直接把tail節(jié)點(diǎn)替換成當(dāng)前節(jié)點(diǎn)的prev節(jié)點(diǎn),得到如下AQS:
在這里插入圖片描述
緊接著第10行,就是把前一個節(jié)點(diǎn)的下一個節(jié)點(diǎn)設(shè)置為空,也就是把ThreadC的next設(shè)置為空:

這樣其實(shí)就相當(dāng)于把ThreadD移除了,這里個人認(rèn)為可以加上node.prev=null幫助GC。
-
當(dāng)前節(jié)點(diǎn)不是tail節(jié)點(diǎn),且不是head節(jié)點(diǎn)的下一個節(jié)點(diǎn)
假如當(dāng)前節(jié)點(diǎn)是ThreadC,這里的if中的13-16行的判斷都是為了確定前一個節(jié)點(diǎn)狀態(tài)是-1且thread不為null,如果后一個節(jié)點(diǎn)也是有效的,那么就通過CAS將ThreadB的next節(jié)點(diǎn)設(shè)置為ThreadD:
在這里插入圖片描述
這里到這一步其實(shí)就可以了,因?yàn)槊看螁拘训臅r候都會執(zhí)行無效節(jié)點(diǎn)的清除,而且喚醒是根據(jù)next往后移動的,這里根據(jù)next找不到ThreadC節(jié)點(diǎn)了。
然后22行就是把當(dāng)前節(jié)點(diǎn)的下一個節(jié)點(diǎn)設(shè)置為自己:

- 當(dāng)前節(jié)點(diǎn)不是tail節(jié)點(diǎn),是head節(jié)點(diǎn)的下一個節(jié)點(diǎn)
這里其實(shí)就相當(dāng)于是說當(dāng)前節(jié)點(diǎn)為第一個可以被喚醒的節(jié)點(diǎn),因?yàn)槲覀兊膆ead節(jié)點(diǎn)是哨兵節(jié)點(diǎn)。
這種就直接執(zhí)行unparkSuccessor(Node)方法就行了,這個方法上面講過了,里面會有清除無效節(jié)點(diǎn)的操作。
總結(jié)
分析到這里ReentrantLock和AQS就分析完了,上面的公平鎖和非公平鎖本來也想放在后面介紹的,因?yàn)槠邢蘧筒粶?zhǔn)備再去分析公平鎖了,如果有確實(shí)想知道的,可以給我評論留言,謝謝大家。
下一篇,將分析Condition隊(duì)列,感興趣的
請關(guān)注我,一起學(xué)習(xí)進(jìn)步