閱讀 JDK 源碼:AQS 中的共享模式

AbstractQueuedSynchronizer,簡稱 AQS,是一個用於構建鎖和同步器的框架。
上一篇文章 介紹了 AQS 的數據結構和獨佔模式的實現原理,本篇介紹 AQS 共享模式的實現原理。java

本文基於 jdk1.8.0_91

1. 共享模式

獨佔模式下,只要有一個線程佔有鎖,其餘線程試圖獲取該鎖將沒法取得成功。
共享模式下,多個線程獲取某個鎖可能(但不是必定)會得到成功。node

1.1 獲取鎖-acquireShared

共享模式下獲取鎖/資源,無視中斷segmentfault

java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireShared數據結構

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
  1. tryAcquireShared:獲取共享鎖/資源,獲取失敗則進入下一步。
  2. doAcquireShared:進入同步隊列中等待獲取鎖/資源。

1.1.1 tryAcquireShared

嘗試獲取資源,具體資源獲取方式交由自定義同步器實現。框架

java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquireSharedoop

protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

對於返回值:ui

  • 負數:獲取資源失敗,準備進入同步隊列;
  • 0:獲取資源成功,但沒有剩餘可用資源;
  • 正數:獲取資源成功,能夠喚醒下一個等待線程;

1.1.2 doAcquireShared

進入同步隊列,自旋判斷是否能獲取鎖,不然進入阻塞。線程

/**
 * Acquires in shared uninterruptible mode.
 * @param arg the acquire argument
 */
private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED); // 在隊列中加入共享模式的節點
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);    // 若是上一個節點是頭結點,則嘗試獲取共享資源,返回剩餘的資源數量
                if (r >= 0) {
                    setHeadAndPropagate(node, r); // 設置當前節點爲新的頭節點(dummy node),並喚醒後繼共享節點
                    p.next = null; // help GC     // 舊的頭節點出隊
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) && // 上一個節點不是頭節點,須要判斷是否進入阻塞:1. 不能進入阻塞,則重試獲取鎖。2. 進入阻塞
                parkAndCheckInterrupt())                 // 阻塞當前線程。當從阻塞中被喚醒時,檢測當前線程是否已中斷,並清除中斷狀態。接着繼續重試獲取鎖。
                interrupted = true;                      // 標記當前線程已中斷
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

共享模式的 doAcquireShared 方法,與獨佔模式的 acquireQueued 相似,節點加入同步隊列以後進行自旋,執行兩個判斷:code

  1. 可否獲取鎖
  2. 可否進入阻塞

不一樣的地方是:blog

  • acquireQueued 中使用 setHead 設置頭節點;
  • doAcquireShared 使用 setHeadAndPropagate 設置頭節點以後,須要判斷是否喚醒後繼節點。

也就是說:

  • 共享模式,獲取鎖成功,或者釋放鎖成功,都須要通知後繼節點。
  • 獨佔模式,釋放鎖成功,才須要通知後繼節點。

setHeadAndPropagate

將當前節點設置爲新的頭節點。
若是共享資源有盈餘,喚醒後續等待中的共享節點。

/**
 * Sets head of queue, and checks if successor may be waiting
 * in shared mode, if so propagating if either propagate > 0 or
 * PROPAGATE status was set.
 *
 * @param node the node
 * @param propagate the return value from a tryAcquireShared    // 共享資源的剩餘數量
 */
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    // 判斷是否喚醒後繼節點
    if (propagate > 0 || h == null || h.waitStatus < 0 || // 若無剩餘資源,則校驗舊的頭節點h的狀態(PROPAGATE或SIGNAL,均<0)
        (h = head) == null || h.waitStatus < 0) {         // 若其餘線程修改了head,取新head做爲前繼節點來校驗
        Node s = node.next;
        if (s == null || s.isShared()) // node.next != null 時,這裏限制了只會喚醒共享節點!
            doReleaseShared(); // 喚醒後繼節點
    }
}

若是知足下列條件能夠嘗試喚醒下一個節點:

  1. 有剩餘資源(propagate > 0),或者頭節點的狀態是PROPAGATE(waitStatus < 0)
  2. 後繼節點是等待中的共享節點,或者後繼節點爲空

可能會形成沒必要要的喚醒,可是通常發生在大量地爭奪 acquires/releases 之時,而這種狀況下,線程遲早都會被喚醒。

doReleaseShared

喚醒後繼節點(共享模式下,當前線程獲取鎖成功、釋放鎖以後,均可能會調用該方法)。

注意:頭節點是共享節點,可是這個方法不會區分後繼節點是不是共享節點。

java.util.concurrent.locks.AbstractQueuedSynchronizer#doReleaseShared

/**
 * Release action for shared mode -- signals successor and ensures
 * propagation. (Note: For exclusive mode, release just amounts
 * to calling unparkSuccessor of head if it needs signal.)
 */
// 共享模式下的 release 操做:在足夠的資源下,喚醒後繼節點,傳播信息(資源盈餘,可共享)
// 互斥模式下的 release 操做:只會喚醒隊列頭部須要喚醒的一個後繼節點(見 AbstractQueuedSynchronizer#unparkSuccessor)
private void doReleaseShared() { 
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {    // 頭節點不爲空,且具備後繼節點
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {     // 若是頭節點狀態是 SIGNAL,嘗試改成 0
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases // CAS失敗,從新自旋
                unparkSuccessor(h);      // 喚醒head的後繼節點
            }
            else if (ws == 0 &&          // 若是頭節點狀態是 0,嘗試改成 PROPAGATE。
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 
                continue;                // loop on failed CAS // CAS失敗,從新自旋
        }
        if (h == head)                   // loop if head changed // 校驗頭節點是否發生變化,若變化了則從新校驗最新頭節點的狀態
            break;
    }
}

代碼流程:

  1. 每次自旋都獲取最新的頭節點 head,若是 head 不爲空,且具備後繼節點,則進入下一步判斷。
  2. SIGNAL → 0 :若是頭節點狀態是 SIGNAL(說明後繼節點阻塞中,等待喚醒):
    ① 改成 0 失敗,從新自旋;
    ② 改成 0 成功,喚醒後繼節點,進入第 4 步。
  3. 0 → PROPAGATE :若是頭節點狀態是 0(後繼節點自旋中未阻塞,或者後繼節點已取消):
    ① 改成 PROPAGATE 失敗,從新自旋;
    ② 改成 PROPAGATE 成功,進入第 4 步。
  4. 修改頭節點狀態成功(SIGNAL → 0 或 0 → PROPAGATE),若是該過程當中頭節點沒有發生變化,結束自旋。

關於 PROPAGATE 狀態

爲何當前節點狀態由 0 改成 PROPAGATE 失敗,須要繼續自旋?

  • 後繼節點在同步隊列中自旋時,執行 shouldParkAfterFailedAcquire 看到前繼節點狀態是 0 或 PROPAGATE,都會改成 SIGNAL。
  • 因此這裏 CAS 失敗,多是後繼節點的修改形成的,須要從新校驗當前節點狀態。
  • 若下一次檢查到當前節點狀態爲 SIGNAL,便可喚醒後繼節點。

爲何當前節點狀態由 0 改成 PROPAGATE 成功,就再也不喚醒後繼節點了呢?

  1. 只有 SIGNAL 才須要主動喚醒後繼節點。
  2. 當前節點的狀態從 0 設爲 PROPAGATE,此時後繼節點多是在同步隊列中自旋中,並未阻塞,無需喚醒;也有可能後繼節點已取消,也無需喚醒。
  3. 當前節點設置狀態爲 PROPAGATE 以後,若處於自旋之中的後繼節點獲取鎖成功(見 doAcquireShared)以後,因爲頭節點狀態爲 PROPAGATE < 0(見 setHeadAndPropagate),會繼續喚醒向下一個節點。

1.2 釋放鎖-releaseShared

共享模式下釋放鎖/資源

java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 釋放共享鎖/資源
        doReleaseShared(); // 釋放鎖/資源成功,喚醒隊列中的等待節點
        return true;
    }
    return false;
}
  1. tryReleaseShared:嘗試釋放共享鎖/資源,釋放成功則進入下一步。
  2. doReleaseShared:釋放鎖/資源成功,喚醒隊列中的等待節點。

java.util.concurrent.locks.AbstractQueuedSynchronizer#tryReleaseShared

protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

相關閱讀:
閱讀 JDK 源碼:AQS 中的獨佔模式
閱讀 JDK 源碼:AQS 中的共享模式
閱讀 JDK 源碼:AQS 對 Condition 的實現

做者:Sumkor

相關文章
相關標籤/搜索