??我們在前文ReentrantLock-NonfairSync原始碼逐行深度分析(上)中分析了NonfairSync獲取鎖和阻塞執行緒的入隊邏輯,也就是定義在AQS中的acquire方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
??從前文中了解到,addWaiter方法會將Thread包裝成一個Node物件,然后通過死回圈(自旋)+CAS的方式保證node一定能夠入隊成功,入隊成功之后會回傳對應的node,然后當做入參傳入acquireQueued方法中,該方法同樣定義在AQS中:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//標識是否被中斷
boolean interrupted = false;
//自旋
for (;;) {
//獲取node節點的前驅節點
final Node p = node.predecessor();
//如果前驅節點是head節點,那么這里在阻塞之前再次嘗試去獲取鎖
if (p == head && tryAcquire(arg)) {
//如果獲取鎖成功,那么將這個node節點設定為新的頭節點
//setHead方法會清空node的thread和prev屬性,并將node賦值到head
setHead(node);
//把舊頭結點的后驅指標設定為空
p.next = null; // help GC
//到這里相當于將舊的那個頭結點孤立了,沒有指標指向它,等待被GC回收
failed = false;
//回傳是否被中斷喚醒,明顯這里是false
return interrupted;
}
//下面是阻塞和為喚醒做準備的操作
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
??這里同樣是自旋邏輯,首先會判斷當前node的前驅節點是否是head節點,如果是,那么不會立即阻塞當前node代表的執行緒,而是再次嘗試去獲取鎖,這個邏輯是什么意思呢?其實很好理解,如果這個條件成立,那就代表此時這個node是CLH的第一個真實排隊節點,佇列中的第二個節點(head頭結點是一個空的Node),此時可能鎖已經被持鎖執行緒釋放了,那么這個排在首位的執行緒其實不需要被阻塞,直接獲取鎖就行了,但是也有可能會獲取鎖失敗,比如現在是非公平鎖的場景,即使node是第一個排隊的節點,但此時又有一個新的執行緒來競爭鎖,由于是非公平的,那么這個鎖就可能被這個新來的執行緒獲取,node還是需要被阻塞,如果獲取鎖成功,那么成功節點就需要”出隊“,但是這里的出隊并不是真正的出隊,其執行的操作是:將節點的thread和prev屬性置空,把head節點設定為當前節點,將舊head節點的next指標斷掉,假如原本是這樣的狀態(t0已經被阻塞,head節點的waitStatus為SIGNAL):

??現在t0被喚醒獲取鎖成功,對應的node從CLH"出隊"之后的狀態:

??可以看到,node節點并沒有真正出隊,而是通過一系列欄位置空和斷開指標操作,node節點變成了新的head節點,而舊的head節點由于沒有參考,會被GC清理掉,當然,如果獲取鎖失敗,那么還是需要進入阻塞狀態,
??接下來看看阻塞的邏輯,這里涉及到了兩個方法的呼叫:shouldParkAfterFailedAcquire和parkAndCheckInterrupt,如果這兩個方法都回傳true,那么會將interrupted欄位設定為true,然后繼續下一次回圈,首先來看shouldParkAfterFailedAcquire方法,注意該方法呼叫傳入的pred引數是node節點的前驅節點,
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//pred為node節點的前驅節點
//獲取前驅結點pred的waitStatus值
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//ws == -1
return true;
if (ws > 0) {
do {
//如果waitStatus為1(CANCELLED)
//那么需要移除pred節點,如果pred的前驅節點還是為CANCELLED
//那么繼續移除,直到節點為非CANCELLED狀態
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//將pred的waitStatus欄位設定為-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
??該方法主要是針對waitStatus的一系列操作,關于Node物件的waitStatus欄位,前面提到過,其定義了4個值:
-1 :SIGNAL:可被喚醒
1:CANCELLED:需要洗掉
-2:CONDITION:條件等待
-3:PROPAGATE:無條件傳播
??當然還有一個默認的0值,關于1、-2、-3幾個值,在本文中不用去關心,后續在總結其它AQS相關實作的工具時遇到再談,這里只需要關注默認值0和-1,初始狀態ws的值為默認值0,那么將會走到CAS修改waitStatus的邏輯,將前驅結點的ws的值修改為-1,然后這個方法回傳false(關于ws大于0,當前只定義了一個大于0的值,就是CANCELLED,本文不用理會,這里做的邏輯處理在代碼中也給出了注釋,就不再多言~),
??shouldParkAfterFailedAcquire方法回傳false之后,緊接著的parkAndCheckInterrupt方法也就不會執行,那么在acquireQueued方法中會進入下一次回圈,這里我們就先不考慮node是首個排隊節點的情況,會再次進入shouldParkAfterFailedAcquire方法的邏輯,只是此時前驅節點的ws的值成了-1,ws == Node.SIGNAL回傳true,那么這個方法此時就會回傳true,那么就會接著執行parkAndCheckInterrupt方法,該方法很簡單,定義如下:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
??通過LockSupport.park方法實作了執行緒的阻塞,方法體中關于回傳當前執行緒中斷狀態的邏輯我們暫時不管,現在先來重新思考shouldParkAfterFailedAcquire和parkAndCheckInterrupt兩個方法的邏輯,經過前面的分析,shouldParkAfterFailedAcquire會執行兩次(不考慮pread==head并且獲取鎖成功的情況,這時節點不會再阻塞),第一次將node的前驅節點的waitStatus設定為SIGNAL(-1),方法回傳false,第二次判斷到waitStatus等于-1,方法回傳true,進而進入parkAndCheckInterrupt方法通過park進行阻塞,所以是node節點的前驅節點的waitStatus的取值影響node節點是否會進入park阻塞,這樣做的意義暫時不談,先知道這么個事兒,等我們看了釋放鎖的邏輯之后再回過頭來看,
??那現在就來看ReentrantLock的unlock方法:
public void unlock() {
sync.release(1);
}
??呼叫的是AQS的release方法:
public final boolean release(int arg) {
//嘗試釋放鎖
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
??該方法的布爾型別回傳值表示釋放鎖是否成功,先不管失敗的情況,直接看看tryReleaes方法是如何實作的,該方法在AQS中也是一個空方法,具體實作在子類中,這里我們看看ReentrantLock類中的實作(Sync):
protected final boolean tryRelease(int releases) {
//releases傳入的是1
//將當前state的值減去1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
//只能是持鎖執行緒才能釋放鎖,否則拋出例外
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//如果state減為0才表示釋放鎖成功
//對于鎖重入的場景,必須有對應重入次數的釋放動作
free = true;
//鎖釋放成功,將exclusiveOwnerThread設定為空
setExclusiveOwnerThread(null);
}
//更新state屬性
setState(c);
return free;
}
??鎖釋放的邏輯很簡單,看代碼中的注釋就行,這里不再贅述,假設現在鎖釋放成功,回傳的free為true,那么就需要執行阻塞執行緒的喚醒動作:
if (tryRelease(arg)) {
//獲取頭結點
Node h = head;
if (h != null && h.waitStatus != 0)
//這里要求頭結點的waitStatus!=0才會去嘗試喚醒阻塞執行緒
unparkSuccessor(h);
//回傳true,表示鎖釋放成功,如果head為null,或head.waitStatus==0,那么不會去嘗試喚醒阻塞執行緒
return true;
}
??釋放鎖和喚醒阻塞執行緒雖然都由持鎖執行緒處理,但是這兩個是獨立的動作,按照代碼邏輯來看,它們允許不同時發生,判斷依據就是waitStatus欄位,對應到前面提到的,執行緒在阻塞之前,會有兩次回圈,第一次會先把前驅節點的waitStatus修改為SIGNAL(-1),然后第二次才會進入parkAndCheckInterrupt方法進行阻塞,注意這里喚醒執行緒是從CLH的頭結點開始喚醒,所以需要判斷head的waitStatus欄位是否不為默認值0,以下狀態表示t0執行緒可以嘗試被喚醒:

??此時head節點的waitStatus==-1,那么可以進入unparkSuccessor方法嘗試喚醒阻塞的執行緒,該方法同樣定義在AQS中:
private void unparkSuccessor(Node node) {
//此時傳入的node是head節點
int ws = node.waitStatus;
if (ws < 0)
//如果waitStatus小于0,則需要重置其狀態
compareAndSetWaitStatus(node, ws, 0);
//需要喚醒的是node的后驅節點
Node s = node.next;
if (s == null || s.waitStatus > 0) {
//如果node沒有后驅節點,或者后驅節點的waitStatus屬性大于0
//目前大于0的值就只定義了1,表示CANCELLED,需要被移除
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
//從后往前找,找到最靠近隊頭的一個waitStatus<=0的節點
if (t.waitStatus <= 0)
//CANCELLED狀態的節點不能被喚醒
s = t;
}
if (s != null)
//找到了需要喚醒的節點,呼叫unpark喚醒節點對應的執行緒
LockSupport.unpark(s.thread);
}
??前面介紹了在shouldParkAfterFailedAcquire方法中有對應CANCELLED節點的移除邏輯,這里也暫時不用考慮太多,只需要知道我們找到了一個需要被喚醒的執行緒,然后通過unpark方法將其喚醒,而這里被喚醒的節點就是head節點的后驅節點,
??那么當對應阻塞的執行緒被喚醒之后呢?我們回到執行緒阻塞的代碼段:
private final boolean parkAndCheckInterrupt() {
//執行緒阻塞在這里
LockSupport.park(this);
//回傳的是執行緒是否被中斷,該方法會重置中斷標志
return Thread.interrupted();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//阻塞執行緒被喚醒后,會繼續回圈,其前驅節點是head節點,才會嘗試繼續獲取鎖
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
//執行緒成功獲取到鎖,failed引數設定為false
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//如果執行緒是被中斷喚醒,那么將中斷表示interrupted設定為true
interrupted = true;
}
} finally {
if (failed)
//在這里執行緒如果重新獲取到鎖,failed不會是true
cancelAcquire(node);
}
}
??這里有幾個非常重要的點:
??第一個是阻塞執行緒被喚醒之后,不是立馬就把鎖給它,而是會繼續執行回圈體,再執行一次:判斷前驅結點是否為head,然后嘗試通過CAS去競爭鎖,對于非公平鎖的情況下,如果持鎖執行緒將鎖釋放了,并不能保證被喚醒執行緒一定能拿到鎖,因為此時可能有一個新的執行緒來競爭鎖,那么這個鎖就可能被這個新執行緒獲取到,被喚醒的執行緒還需要再次陷入阻塞,關于這點在前面分析acquireQueued方法的時候已經有過說明,在前面unparkSuccessor方法喚醒阻塞執行緒的邏輯中,將head的waitStatus的值又重置為了初始值0,那么這里如果被喚醒執行緒競爭失敗,需要重新陷入阻塞,會又重復兩次回圈,先將前驅節點(這里是head)的waitStatus設定為SIGNAL(-1),然后再陷入阻塞,這樣不斷回圈~
??第二個是執行緒中斷的處理,執行緒在方法parkAndCheckInterrupt被阻塞,然后喚醒后,回傳了當前執行緒是否被中斷的標志,關于執行緒中斷,可以理解為是一個信號,以前終結一個執行緒需要呼叫thread.stop方法,該方法會呼叫native方法stop0,這個方法很暴力,執行緒會被直接kill,如果執行緒正在執行某些操作,也就會被暴力打斷,所以后面提供了一個優雅一點的方式:interrupt,執行緒被中斷后,開發人員可以獲取到該標識:Thread.interrupted(),然后優雅地退出執行緒,需要注意的是,呼叫該方法會重置執行緒的中斷狀態:
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
??所以被park阻塞的執行緒,既可以被upark喚醒,也可以被interrupt喚醒,在此處的實作邏輯中,可以看到不論interrupt是true還是false都沒有影響,仍然會不斷回圈重試,直到成功獲取到鎖,最終interrupted作為acquireQueued方法的回傳值回傳給呼叫者,在前文已經分析過了,其呼叫者是AQS的acquire方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//acquireQueued方法回傳的是執行緒是否被中斷過
//如果被中斷過,則需要在這里進行一次自我中斷
selfInterrupt();
}
static void selfInterrupt() {
//執行緒自己中斷自己
Thread.currentThread().interrupt();
}
??為什么需要自己中斷自己?原因就是前面提到的,Thread.interrupted()方法會重置執行緒的interrput狀態,這里需要把中斷狀態向外傳遞,使得外層呼叫者能正確感知到執行緒被中斷過,以便做出相應的處理,所以需要自己中斷自己重新設定中斷狀態,
??第三是acquireQueued方法中斷finally 代碼塊:
finally {
if (failed)
cancelAcquire(node);
}
??在此處的場景中,這個failed好像除了未知的例外發生之外,永遠不會為true,因為即使執行緒被中斷喚醒了,也會繼續競爭鎖,競爭失敗會繼續陷入阻塞,競爭成功,那么failed會被設定為false,事實上,ReentrantLock除了lock()方法之外,還有一個lockInterruptibly方法,看方法名可能就知道了,這個方法是支持中斷的,看看其在AQS中的實作:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
//如果執行緒被中斷,那么直接拋出中斷例外
throw new InterruptedException();
if (!tryAcquire(arg))
//如果首次嘗試獲取鎖失敗,那么呼叫doAcquireInterruptibly方法阻塞
doAcquireInterruptibly(arg);
}
??如果執行緒被中斷,那么直接拋出中斷例外,否則就嘗試呼叫tryAcquire方法獲取鎖,如果獲取鎖失敗,那么呼叫方法doAcquireInterruptibly完成執行緒的阻塞作業(前面介紹的lock邏輯是通過acquireQueued方法實作的),來看看這個方法:
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//和前面分析的acquireQueued方法一樣,自旋CAS保證入隊成功
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
//同樣的自旋獲取鎖
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
//同樣的修改waitState和阻塞邏輯
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//但是這里是拋出例外,前面是設定interrupted = true
//拋出例外后,failed 還是為初始值true,那么就會執行finally代碼塊中的cancelAcquire方法
throw new InterruptedException();
}
} finally {
if (failed)
//如果前面拋出InterruptedException就會執行這個方法
cancelAcquire(node);
}
}
??可以看到doAcquireInterruptibly方法和acquireQueued方法"唯一"的區別就是,doAcquireInterruptibly沒有回傳值,在這里面如果阻塞執行緒被中斷喚醒,那么不會繼續參與鎖的競爭,而是直接拋出InterruptedException,然后會在finally代碼塊中執行cancelAcquire方法;而acquireQueued方法即使執行緒被中斷喚醒,那么也只是將中斷狀態保存下來,然后會繼續參與鎖的競爭,最侄訓取到鎖之后,會把中斷標識回傳給方法呼叫者,最終通過selfInterrupt執行緒自我中斷重新打上中斷標記,以便更外層的呼叫者能夠獲取到該狀態,
??那么我們來看看cancelAcquire方法:
private void cancelAcquire(Node node) {
if (node == null)
//判空
return;
//置空當前節點的thread,因為被中斷后就不需要了,也不需要被喚醒了
node.thread = null;
//獲取node的前驅節點
Node pred = node.prev;
while (pred.waitStatus > 0)
//跳過node所有waitStatus>0(也就是CANCELLED)的前驅節點
//將node的前驅指標指向最近的一個非CANCELLED節點pred
node.prev = pred = pred.prev;
//獲取pred的后驅節點
Node predNext = pred.next;
//將當前node節點的waitStatus設定為CANCELLED
node.waitStatus = Node.CANCELLED;
if (node == tail && compareAndSetTail(node, pred)) {
//情況1:node是尾結點并且成功通過CAS將pred設定為尾結點(可能有其它新執行緒節點入隊,導致node不再是尾結點)
//將pred的后驅指標置空:相當于移除了當前node和node的所有CANCELLED前驅節點
compareAndSetNext(pred, predNext, null);
} else {
//node不是尾結點,或者本來是尾結點,但是CAS的時候被其它執行緒更新了CLH(上面compareAndSetTail失敗),node變成了非尾節點
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//情況2:pred不是head節點,并且pred是有效的節點(waitStatus不是CANCELLED,并且是SIGNAL或被成功修改為SIGNAL,thread不為空)
//如果pred的waitStatus不等于SIGNAL,會嘗試將其修改為SIGNAL,表示下個節點可被喚醒
//由于節點的waitStatus可能被其它執行緒修改,所以使用了CAS操作
Node next = node.next;
if (next != null && next.waitStatus <= 0)
//當node==tail,但是CAS替換tail的時候失敗了,邏輯也可能走到這里,所以next可能為null,需要判空
compareAndSetNext(pred, predNext, next);
} else {
//情況三:pred是head節點或者無效的節點
unparkSuccessor(node);
}
//將node的next指向自己
node.next = node; // help GC
}
}
??這個方法代碼比較復雜,但是實作的邏輯很簡單,就是根據節點的不同位置采取不同的邏輯,將該節點從CLH佇列中移除,需要的時候還會嘗試喚醒后驅節點,代碼中給出了相應的注釋,這里也通過畫圖再詳細解釋一下其邏輯,
??首先是注釋中提到的,跳過所有waitStatus>0(也就是CANCELLED)的前驅節點,重新設定node的前驅指標,假設初始狀態是這樣(注意這里不關注waitState<=0的具體狀態,所以圖中節點只畫了1和0兩個狀態):
??跳過所有CANCELLED前驅節點,重新設定前驅指標,置空thread,接著獲取pred的后驅節點,然后將node的waitStatus設定為CANCELLED:

??這些邏輯都是做的準備作業,接下來對于三個情況有不同的處理邏輯,
- 情況1:當前node節點是尾結點(tail),那么就直接移除node節點,還順帶把node的所有CANCELLED前驅節點一并移除,移除的操作是,先將pred設定為tail,然后將pred的next置空,對應到上圖就成了這個狀態:

??最后兩個waitStatus為1的節點(包括node)被邏輯移除,沒有了GC Root參考鏈參考,可以被GC清理,當然,CAS更新尾結點可能會失敗,如果失敗,那么就會走入下面的邏輯, - 情況2:pred不是head節點,并且pred是有效的節點,那么先獲取node的next節點,如果該節點是有效節點,那么嘗試將pred的next指向這個node的next,最終將node的next指向自己:

??可以看到,中間那個無效節點的指標已經斷了,但是node的后驅節點的prev指標仍然指向node,不過沒有影響,因為在其后面入隊的節點準備阻塞或者中斷后會從后往前遍歷節點,同時"移除"無效節點, - 情況3:pred是head節點或者無效的節點,此時直接嘗試喚醒node的后驅節點,最后將node的next指向自己,
這里需要注意,情況3中呼叫unparkSuccessor方法的目的是什么?我們假設這里unparkSuccessor方法呼叫之前是以下狀態:

??現在喚醒t1執行緒,t1執行緒會在acquireQueued方法中繼續回圈,顯然此時t1的prev節點不是head節點,所以會進入shouldParkAfterFailedAcquire方法修改前驅節點的waitStatus,但是發現前驅節點(也就是node)的watiStatus大于0 ,所以會跳過該節點,前面已經分析過shouldParkAfterFailedAcquire方法,這里就不詳細說明了,只貼一下此處關心的代碼:
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
}
return false;
??經過這個操作之后,狀態變成了:

??此時shouldParkAfterFailedAcquire方法回傳的false,接著又進行一次回圈,這次回圈發現t1節點的prev節點是head,那么t1就會嘗試去競爭鎖,如果競爭失敗,則再次陷入阻塞狀態,那么為什么不直接修改指標,要通過喚醒t1執行緒去修改指標呢?試想一下,如果持鎖執行緒unlock釋放了鎖,那么需要從head喚醒一個阻塞的執行緒,由于node是head的后驅節點,所以node會被喚醒,而node對應的執行緒卻在方法回傳之前突然又出現例外(比如tryAcquire方法中獲取鎖時拋出了例外),也就相當于喚醒失敗了,沒有成功獲取到鎖,從而進入了cancelAcquire方法,那么這個node會在cancelAcquire中被移除,但是這里要注意,node移除之后,本次unlock就相當于沒有喚醒任何執行緒(因為node由于例外持鎖失敗),如果不來新的執行緒競爭鎖,就可能出現沒有執行緒持有鎖,但是卻有很多執行緒被阻塞排隊等待鎖被釋放喚醒它們的情況(純屬個人猜測~),
??另外,關于unparkSuccessor方法前文已經介紹過了,這里不再贅述,就是前文邏輯中入參是head,此處是node,但是為什么尋找有效節點是從后往前找,不是從前往后找呢?結合到無效節點出隊的邏輯,可以發現一些無效節點的指標一開始并沒有全部斷開,而是保留了部分后驅節點對其的前置指標,從后往前遍歷可以根據這個前置指標找到這些無效節點,完成真正的出隊,也就相當于出隊動作被延遲了,
總結
??Doug Lea將眾多邏輯都凝練到AQS中,單獨看某個函式的時候,可能會覺得有些邏輯明明可以有更簡單的實作,有些邏輯又是莫名其妙,因為這些函式邏輯考慮了AQS的眾多應用場景,所以需要對這塊兒有個整體上的把握才能更好的理解其設計,正是由于這個原因,本文在分析函式的時候大多都只考慮當前分析的場景,不然可能會很懵逼,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/274050.html
標籤:java
