引子
前文《面試必考AQS-排它鎖的申請與釋放》已經對AQS中對於排它鎖的申請與釋放流程進行了總結。而對於申請與釋放,在AQS中提現的是與鎖並無關係,而是針對同步隊列的操作,向同步隊列中添加、移除Node實例對象,操作Node中的線程對象。而我們日常使用的鎖類,只是表象,何時可以加鎖、解鎖,達到何種條件纔算加鎖成功、解鎖成功,這纔是AQS鎖實現的功能。接下來將從源碼層面看一下,排它鎖的申請與釋放在AQS中具體是怎樣實現的。
排它鎖的申請
再看一次申請流程的大致方法調用:
public final void acquire(int arg){...} // 獲取排它鎖的入口
# protected boolean tryAcquire(int arg); // 嘗試直接獲取鎖
final boolean acquireQueued(final Node node, int arg) {...} // AQS中獲取排它鎖流程整合
private Node addWaiter(Node mode){...} // 將node加入到同步隊列的尾部
# protected boolean tryAcquire(int arg); // 如果當前node的前置節點pre變爲了head節點,則嘗試獲取鎖(因爲head可能正在釋放)
private void setHead(Node node) {...} // 設置 同步隊列的head節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {...} // 如果獲取鎖失敗,則整理隊中節點狀態,並判斷是否要將線程掛起
private final boolean parkAndCheckInterrupt() {...} // 將線程掛起,並在掛起被喚醒後檢查是否要中斷線程(返回是否中斷)
private void cancelAcquire(Node node) {...} // 取消當前節點獲取排它鎖,將其從同步隊列中移除
static void selfInterrupt() {...} // 操作將當前線程中斷
先看一下入口方法acquire(int arg)的代碼。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
源碼上是先調用tryAcquire()方法,如果返回false,則進入acquireQueued();由於tryAcquire()是由子類實現的,我們暫時先不看,留到以後梳理AQS實現類時再串講。
那麼從tryAcquire()從方法名來講就是“嘗試獲取鎖”,先假設return=false,也就是“嘗試獲取鎖失敗”,繼續執行調用acquireQueued()。
在進入acquireQueued()之前,會先調用一次addWaiter(),傳入了一個Node.EXCLUSIVE對象;Node.EXCLUSIVE在前文有介紹,是Node類中一個實例變量成員,用於標記排他類型的Node實例。
那麼看一下addWaiter()方法,直接看一下方法內註釋吧。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 用入參mode=Node.EXCLUSIVE,創建了一個Node實例。
// 將新創建的節點 加入 同步隊列的尾部
Node pred = tail; // 拿到同步隊列尾指針
if (pred != null) {
node.prev = pred; // 追加到尾部
if (compareAndSetTail(pred, node)) { // cas方式,更新尾指針指向 新節點node
pred.next = node;
return node;
}
}
// 如果上述方式加入隊列失敗,進入enq(node)方法。
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) { // 無限嘗試
Node t = tail;
if (t == null) {
// 尾結點不能指向爲null,說明隊列爲null,需要初始化同步隊列的頭尾指針
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 這段代碼的含義,與addWaiter()中的一致,也就是不停嘗試將節點加入到同步隊列
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
那麼addWaiter()方法就一個目的:將新的node加入到同步隊列的尾部,並將node返回。
接下來繼續調用acquireQueued()方法,我們看看它做了什麼:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;// 一個狀態標記,goto t1
try {
boolean interrupted = false; // 一個狀態標記,goto t2
for (;;) {
final Node p = node.predecessor(); // 獲取node的前置節點p,前文有介紹過
if (p == head && tryAcquire(arg)) { // 如果p是頭節點,立即嘗試獲取鎖(注意只有隊列head的下一個節點才能去獲取鎖)
setHead(node); // 如果返回true,說明獲取鎖成功,設置head爲當前節點(說明原有head已經釋放鎖)
p.next = null; // help GC
failed = false;
return interrupted; // t2',將中斷標識返回
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; // t2,是否中斷線程的標記,goto t2'
}
} finally {
if (failed) // t1,狀態標識的值,決定是否調用cancelAcquire()
cancelAcquire(node);
}
}
流程比較清晰,一個for(;;),循環嘗試獲取鎖,如果節點到達同步隊列的head之後,則嘗試申請鎖,申請成功返回,否則調用shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),我們繼續往下看。
// 入參:pred 就是當前節點的前置節點,node就是當前節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 獲取前置節點的狀態
if (ws == Node.SIGNAL) // SIGNAL 代表 節點等待被喚醒,可以理解爲是一種阻塞狀態的標記
/*
* pred節點爲SIGNAL狀態,可以認爲node允許阻塞
*/
return true;
if (ws > 0) {
// 由於ws的狀態只有CANCEL是大於0,因此節點無效,前置節點可以從隊列移除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); // 遍歷同步隊列中,node前面的所有節點,將CANCEL狀態的都移除
pred.next = node;
} else {
// 如果前置節點狀態 小於0,說明是正常節點(但不是SIGNAL),
// 先cas設置爲SIGNAL(標記存在後置節點),然後交給外層for(;;)再次嘗試申請鎖
// else中不返回true的目的是:
// 防止過早進入parkAndCheckInterrupt(),而應該再次嘗試申請鎖(臨近狀態控制)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果shouldParkAfterFailedAcquire()返回true,則進入parkAndCheckInterrupt()方法,這裏只做了一件事,將線程掛起。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 掛起當前線程;由此線程等待被喚醒
// other thread going to work..
return Thread.interrupted(); // 當線程被喚醒後,返回線程是否中斷
}
我們再回到acquireQueued()方法看一下,注意看註釋:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 這裏的工作是:檢查前置節點狀態,如果是SIGNAL,則讓當前node代表的線程掛起
// 如果不是SIGNAL,則會繼續for(;;)努力申請鎖。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果parkAndCheckInterrupt()==true說明線程被中斷,需要設置interrupted標記
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
再看看cancelAcquire()內部實現:
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
// 節點操作取消申請,清空線程對象
node.thread = null;
// 將同步隊列中node前的節點,移出隊列
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// preNext 是清理後的同步隊列的pre節點的下一個有效節點
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// 如果node在隊尾,則將隊尾tail指向pred節點
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);// 並將pred節點的next指向predNext。
// 以上都是在整理同步隊列節點;node在隊尾,直接丟棄即可
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// t3 能進入這裏的條件是:
// 1、前置節點不是head節點
// 並且
// 2、前置節點狀態是SIGNAL 或者 (前置節點狀態小於0 並且 可以修改爲SIGNAL)
// 並且
// 3、前置節點的線程不爲null
// 以上,都是在判定 pred節點是否爲一個有效節點
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
// 將前置節點的next指向當前node的next,從同步隊列中移除node
} else {
// 如果t3 的條件未達到,
// 則手動喚醒node代表的線程
// (假設node還在被掛起,則可以交給acquireQueued()中的for(;;) 重新搞一波)
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
以上,就是申請排它鎖的全部流程,其中比較關鍵的方法,都是涉及到waitStatus的值變化的方法,如shouldParkAfterFailedAcquire()、parkAndCheckInterrupt()、cancelAcquire(),思路要理清才能搞明白。
重新回憶下申請鎖都做了什麼:
1、先交給子類去嘗試申請鎖,如果申請成功了,則結束流程;
2、否則將申請線程包裝爲node節點,並加入同步隊列
3、循環檢查當前節點的前置節點是否爲head,如果是則申請獲取鎖
4、否則檢查節點狀態,並將線程掛起,等待前面有節點釋放鎖後,來主動喚醒該線程
5、將CANCEL狀態的節點清理掉。
鎖既然已經申請到了,那麼不可避免的就要釋放;接下來再分析一下釋放鎖的源碼。
排它鎖的釋放
再看一次釋放流程的大致方法調用:
public final boolean release(int arg) {...} // 釋放排它鎖的入口
# protected boolean tryRelease(int arg); // 嘗試直接釋放鎖
private void unparkSuccessor(Node node) {...} // 喚醒後繼節點
先看入口方法release()的工作:
public final boolean release(int arg) {
if (tryRelease(arg)) { // 同樣先讓子類執行tryRelease()來嘗試釋放鎖,
// 這回是如果成功了繼續
Node h = head;
if (h != null && h.waitStatus != 0)
// head如果不爲null並且狀態不是0,則調用unparkSuccessor()
unparkSuccessor(h);
return true;
}
return false;
}
釋放鎖具體怎麼釋放,交給了子類去操作,那麼說明一點,tryRelease()中並不涉及同步隊列的操作。而unparkSuccessor(h);中做的事情,纔是AQS的關鍵。
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.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 修改node節點狀態爲SIGNAL
/*
* 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.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) { // head的下一個節點,如果爲CANCEL狀態
s = null;
// 清理同步隊列,注意這裏是從尾部開始向前清理的
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 由於是從尾部清理,因此s最後的值應該是同步隊列中第一個waitState<=0的節點
// 那麼unpark()操作的就是同步隊列中第一個等待被喚醒的節點
if (s != null)
LockSupport.unpark(s.thread);
}
以上就是釋放鎖的流程;重新回憶一下釋放鎖到底幹了什麼。
1、先交給子類去嘗試釋放鎖,如果成功則處理同步隊列的節點
2、將同步隊列中的無效節點移除,然後將隊列中node之後第一個有效節點喚醒
其實就這麼簡單,但是代碼很精妙,尤其是從尾部遍歷同步隊列的思路。
尾聲
對於AQS中排它鎖的申請與釋放,思路是比較清晰的,而且行爲方向單一,就是針對同步隊列的入隊與出隊操作,以及在某些情況下針對同步隊列進行遍歷;再加上LockSupport的park和unpark調用,來控制線程狀態。
這就是AQS的一部分,仔細分析源碼,其實沒那麼複雜。