上篇我們講了Java的AQS詳解1--獨(dú)占鎖的獲取及釋放,本篇接著講共享鎖的獲取及釋放。
加鎖
共享鎖加鎖的方法入口為:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared(arg)嘗試獲取鎖,由AQS的繼承類實現(xiàn)。
若返回值為負(fù),證明獲取鎖失敗,緊接著執(zhí)行doAcquireShared(arg)方法。
doAcquireShared
private void doAcquireShared(int arg) {
// 將該線程封裝成共享節(jié)點(diǎn),并追加到同步隊列中
final Node node = addWaiter(Node.SHARED);
// 失敗標(biāo)志
boolean failed = true;
try {
// 中斷標(biāo)志
boolean interrupted = false;
for (;;) {
// 獲取node的前繼節(jié)點(diǎn)
final Node p = node.predecessor();
// 若node的前繼節(jié)點(diǎn)為head節(jié)點(diǎn),則執(zhí)行tryAcquireShared方法嘗試獲取鎖(資源)
if (p == head) {
int r = tryAcquireShared(arg);
// 若返回值>=0,表明獲取鎖成功
if (r >= 0) {
// 將當(dāng)前節(jié)點(diǎn)設(shè)置為head節(jié)點(diǎn),并喚醒后繼節(jié)點(diǎn)
setHeadAndPropagate(node, r);
p.next = null; // help GC
// 如果中斷標(biāo)志位true,響應(yīng)掉中斷
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 若前繼節(jié)點(diǎn)不為head節(jié)點(diǎn)或者前繼節(jié)點(diǎn)為head,但tryAcquireShared獲取鎖失敗
// shouldParkAfterFailedAcquire自旋CAS將node的前繼節(jié)點(diǎn)的狀態(tài)設(shè)置為SIGNAL(-1),并返回true
// parkAndCheckInterrupt將線程阻塞掛起,重新被喚醒后檢查阻塞期間是否被中斷過,將interrupted置為true
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 若線程異常,則放棄獲取鎖
if (failed)
cancelAcquire(node);
}
}
可以看到,doAcquireShared方法和獨(dú)占鎖的acquireQueued方法邏輯類似,主要有2點(diǎn)不同:
- doAcquireShared方法直接將中斷響應(yīng)掉了,而acquireQueued只是返回中斷標(biāo)志,是否響應(yīng)留在了acquire方法中;
- doAcquireShared方法獲取鎖成功之后,除了將當(dāng)前節(jié)點(diǎn)設(shè)置為head之外,還有個喚醒后繼節(jié)點(diǎn)的操作,即setHeadAndPropagate方法。
setHeadAndPropagate
private void setHeadAndPropagate(Node node, int propagate) {
// 原有head節(jié)點(diǎn)備份
Node h = head;
// 將當(dāng)前節(jié)點(diǎn)設(shè)置為head
setHead(node);
// 若propagate>0(有剩余資源)或者原h(huán)ead節(jié)點(diǎn)為null或原h(huán)ead節(jié)點(diǎn)的狀態(tài)值<0
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 獲取node的后繼節(jié)點(diǎn)
Node s = node.next;
// 若后繼節(jié)點(diǎn)為null或者為共享節(jié)點(diǎn),則執(zhí)行doReleaseShared方法繼續(xù)傳遞喚醒操作
if (s == null || s.isShared())
doReleaseShared();
}
}
doReleaseShared
private void doReleaseShared() {
for (;;) {
// 此時的head節(jié)點(diǎn)已經(jīng)被替換為node節(jié)點(diǎn)了
Node h = head;
// 若head不為null且不是tail節(jié)點(diǎn)
if (h != null && h != tail) {
int ws = h.waitStatus;
// 若head節(jié)點(diǎn)狀態(tài)為SIGNAL(-1),則自旋CAS將head節(jié)點(diǎn)的狀態(tài)設(shè)置為0之后,才可以喚醒head結(jié)點(diǎn)的后繼節(jié)點(diǎn)
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
// 執(zhí)行下一次自旋
continue;
unparkSuccessor(h);
}
// 若head節(jié)點(diǎn)狀態(tài)為0,則自旋CAS將節(jié)點(diǎn)狀態(tài)設(shè)置為PROPAGATE(-3)
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
// 執(zhí)行下一次自旋
continue;
}
// head指針在自旋期間未發(fā)生移動的話,跳出自旋
if (h == head)
break;
}
}
為什么最后需要判斷(h==head)才跳出自旋?
想象2種情景:
- 第1種情景
線程thread1自旋CAS將head節(jié)點(diǎn)的狀態(tài)由SIGNAL修改為0,才喚醒后繼線程thread2,當(dāng)執(zhí)行到(h == head)時,假如thread2喚醒后已經(jīng)將head指向自己了,此時(h == head)返回false,thread1繼續(xù)自旋獲取到新的head節(jié)點(diǎn)(thread2);
thread1自旋CAS將新的head節(jié)點(diǎn)(thread2)的狀態(tài)由SIGNAL修改為0,然后去喚醒thread2的后繼線程thread3,當(dāng)執(zhí)行到(h == head)時,假如thread3喚醒后已經(jīng)將head指向自己了,此時(h == head)返回false,thread1繼續(xù)自旋獲取到新的head節(jié)點(diǎn)(thread3),
thread1自旋CAS將新的head節(jié)點(diǎn)(thread3)的狀態(tài)由SIGNAL修改為0,然后去喚醒thread3的后繼線程thread4......
直到某個被喚醒的線程因為獲取不到鎖(資源被用盡)執(zhí)行shouldParkAfterFailedAcquire方法被阻塞掛起,head節(jié)點(diǎn)才沒有發(fā)生改變,此時(h == head)返回true,跳出自旋。
- 第2種情景
線程thread1自旋CAS將head節(jié)點(diǎn)的狀態(tài)由SIGNAL修改為0,才喚醒后繼線程thread2,當(dāng)執(zhí)行到(h == head)時,假如thread2喚醒后還未來得及將head指向自己,此時(h == head)返回true,thread1停止自旋;
thread2喚醒后將執(zhí)行setHeadAndPropagate方法將head指向自己,并最終進(jìn)到doReleaseShared方法的自旋中;
此時,線程thread2自旋CAS將head節(jié)點(diǎn)的狀態(tài)由SIGNAL修改為0,才喚醒后繼線程thread3......
哈哈哈,是不是像thread1一樣又面臨了2種情景。
可以看到,整個喚醒后繼節(jié)點(diǎn)的過程是不斷嵌套,螺旋執(zhí)行的,每個節(jié)點(diǎn)的線程都最大程度的嘗試喚醒其可以喚醒的節(jié)點(diǎn),而且每個線程都是喚醒的head的后繼節(jié)點(diǎn),head指針不斷往后推進(jìn),則被喚醒嘗試獲取共享鎖的線程越多,而新的線程一旦獲取到鎖,其又會執(zhí)行到setHeadAndPropagate-->doReleaseShared的自旋中,加入到喚醒head后繼節(jié)點(diǎn)的聯(lián)盟大軍中,直到無鎖可獲。
所以,整個喚醒后繼節(jié)點(diǎn)的過程如果一場風(fēng)暴一樣,不得不驚嘆這樣的設(shè)計呀,最大程度的詮釋了何為共享,就是"有肉一起吃,有酒一起喝"。
解鎖
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
某線程執(zhí)行tryReleaseShared方法成功后,會釋放掉部分資源,然后執(zhí)行doReleaseShared方法喚醒當(dāng)前head節(jié)點(diǎn)的后繼線程,來參與分享資源。
doReleaseShared方法前面陳述過了,這是個"喚醒風(fēng)暴",它會喚醒所有可以喚醒的人來參與資源的分享。
整個獲取/釋放資源的過程是通過傳播完成的,如最開始有10個資源,線程A、B、C分別需要5、4、3個資源。
- A線程獲取到5個資源,其發(fā)現(xiàn)資源還剩余5個,則喚醒B線程;
- B線程獲取到4個資源,其發(fā)現(xiàn)資源還剩余1個,喚醒C線程;
- C線程嘗試取3個資源,但發(fā)現(xiàn)只有1個資源,繼續(xù)阻塞;
- A線程釋放1個資源,其發(fā)現(xiàn)資源還剩余2個,故喚醒C線程;
- C線程嘗試取3個資源,但發(fā)現(xiàn)只有2個資源,繼續(xù)阻塞;
- B線程釋放2個資源,其發(fā)現(xiàn)資源還剩余4個,喚醒C線程;
- C線程獲取3個資源,其發(fā)現(xiàn)資源還剩1個,繼續(xù)喚醒后續(xù)等待的D線程;
- …
回顧整個共享鎖加鎖和解鎖的過程,可以發(fā)現(xiàn)head指針至關(guān)重要,無論是加鎖成功后執(zhí)行setHeadAndPropagate方法進(jìn)而執(zhí)行doReleaseShared方法,還是線程解鎖時直接執(zhí)行doReleaseShared方法,其均是直接從當(dāng)前隊列的的head節(jié)點(diǎn)的后繼節(jié)點(diǎn)開始"喚醒",而被喚醒的多個線程也是通過(h == head)判斷來決定是否跳出"喚醒自旋"的。
最后,再次感嘆,這個"喚醒風(fēng)暴"設(shè)計得太贊了?。?!