Java的AQS詳解2--共享鎖的獲取及釋放

上篇我們講了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è)計得太贊了?。?!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容