1. 程式人生 > 程式設計 >JDK原始碼分析-AbstractQueuedSynchronizer(2)

JDK原始碼分析-AbstractQueuedSynchronizer(2)

概述


前文「JDK原始碼分析-AbstractQueuedSynchronizer(1)」初步分析了 AQS,其中提到了 Node 節點的「獨佔模式」和「共享模式」,其實 AQS 也主要是圍繞對這兩種模式的操作進行的。

Node 節點是對執行緒 Thread 類的封裝,因此兩種模式可以理解如下:

獨佔模式(exclusive):執行緒對資源的訪問是排他的,即某個時間只能一個執行緒單獨訪問資源;

共享模式(shared):與獨佔模式不同,多個執行緒可以同時訪問資源。


本文先分析獨佔模式下的各種操作,後面再分析共享模式。


獨佔模式


方法概述


獨佔模式下的操作主要有以下幾個方法(可與前面分析的 Lock 介面的方法類比):


1. acquire(int arg)

以獨佔模式獲取資源,忽略中斷;可以類比 Lock 介面的 lock 方法;


2. acquireInterruptibly(int arg)

以獨佔模式獲取資源,響應中斷;可以類比 Lock 介面的 lockInterruptibly 方法;


3. tryAcquireNanos(int arg,long nanosTimeout)

以獨佔模式獲取資源,響應中斷,且有超時等待可以類比 Lock 介面的 tryLock(long,TimeUnit) 方法;


4. release(int arg)

釋放資源,可以類比 Lock 介面的 unlock 方法。


方法分析


1. 獨佔模式獲取資源(忽略中斷)


這幾種獲取資源的方法很多地方是類似的。我們先從 acquire 方法開始分析,如下:

public final void acquire(int arg) {    if (!tryAcquire(arg) &&        acquireQueued(addWaiter(Node.EXCLUSIVE),arg))        selfInterrupt();}

該方法看似很短,其實是內部做了封裝。這幾行程式碼包含了如下四個操作步驟:

1. tryAcquire

2. addWaiter(Node.EXECUSIVE)

3. acquireQueued(final Node node,arg))

4. selfInterrupt

上面的四個步驟不一定全部執行,下面依次進行分析。

step 1: tryAcquire

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

該方法的作用是嘗試以獨佔模式獲取資源,若成功則返回 true。

可以看到該方法是一個 protected 方法,而且 AQS 中該方法直接丟擲了異常,其實是它把實現委託給了子類。這也是 ReentrantLock、CountdownLatch 等類(嚴格來說是其內部類 Sync)的實現功能不同的地方,這些類正是通過對該方法的不同實現來制定了自己的“遊戲規則”。

若 step 1 中的 tryAcquire 方法返回 true,則表示當前執行緒獲取資源成功,方法直接返回,該執行緒接下來就可以“為所欲為”了;否則表示獲取失敗,接下來會依次執行 step 2 和 step 3。

step 2: addWaiter(Node.EXECUSIVE)

private Node addWaiter(Node mode) {    // 將當前執行緒封裝為一個 Node 節點,指定 mode    // PS: 獨佔模式 Node.EXECUSIVE, 共享模式 Node.SHARED    Node node = new Node(Thread.currentThread(),mode);    // Try the fast path of enq; backup to full enq>    Node pred = tail;    if (pred != null) {        node.prev = pred;        // 通過 CAS 操作設定主佇列的尾節點        if (compareAndSetTail(pred,node)) {            pred.next = node;            return node;        }    }    // 尾節點 tail 為 null,表示主佇列未初始化    enq(node);    return node;}

enq 方法:

private Node enq(final Node node) {    for (;;) {        Node t = tail;        // 尾節點為空,表明當前佇列未初始化        if (t == null) { // Must initialize            // 將佇列的頭尾節點都設定為一個新的節點            if (compareAndSetHead(new Node()))                tail = head;        } else {            // 將 node 節點插入主佇列末尾            node.prev = t;            if (compareAndSetTail(t,monospace;'>                t.next = node;                return t;            }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;            // 若上面條件不滿足,即前驅節點不是頭節點,或嘗試獲取失敗            // 判斷當前執行緒是否可以休眠            if (shouldParkAfterFailedAcquire(p,node) &&                parkAndCheckInterrupt())                interrupted = true;    } finally {        if (failed)            cancelAcquire(node);}

若當前節點的前驅節點為頭節點,則會再次嘗試獲取資源(tryAcuqire),若獲取成功,則將當前節點設定為頭節點並返回;否則,若前驅節點不是頭節點,或者獲取資源失敗,則執行如下兩個方法:

private static boolean shouldParkAfterFailedAcquire(Node pred,Node node) {    // 前驅節點的等待狀態    int ws = pred.waitStatus;    // 若前驅節點的等待狀態為 SIGNAL,返回 true,表示當前執行緒可以休眠    if (ws == Node.SIGNAL)        /*         * This node has already set status asking a release         * to signal it,so it can safely park.         */        return true;    // 若前驅節點的狀態大於 0,表示前驅節點處於取消(CANCELLED)狀態    // 則將前驅節點跳過(相當於踢出佇列)    if (ws > 0) {         * Predecessor was cancelled. Skip over predecessors and         * indicate retry.        do {            node.prev = pred = pred.prev;        } while (pred.waitStatus > 0);        pred.next = node;    } else {         * waitStatus must be 0 or PROPAGATE.  Indicate that we         * need a signal,but don't park yet.  Caller will need to         * retry to make sure it cannot acquire before parking.         */        // 此時 waitStatus 只能為 0 或 PROPAGATE 狀態,將前驅節點的等著狀態設定為 SIGNAL        compareAndSetWaitStatus(pred,ws,Node.SIGNAL);    return false;private final boolean parkAndCheckInterrupt() {    // 將當前執行緒休眠    LockSupport.park(this);    return Thread.interrupted();static void selfInterrupt() {    Thread.currentThread().interrupt();public final void acquireInterruptibly(int arg)        throws InterruptedException     // 若執行緒被中斷過,則丟擲異常    if (Thread.interrupted())        throw new InterruptedException();    // 嘗試獲取資源    if (!tryAcquire(arg))        // 嘗試獲取資源失敗        doAcquireInterruptibly(arg);private void doAcquireInterruptibly(int arg)    throws InterruptedException     // 將當前執行緒封裝成 Node 節點插入主佇列末尾    final Node node = addWaiter(Node.EXCLUSIVE);                return;                // 丟擲中斷異常                throw new InterruptedException();public final boolean tryAcquireNanos(int arg,long nanosTimeout)    // 若被中斷,則響應    return tryAcquire(arg) ||        doAcquireNanos(arg,nanosTimeout);static final long spinForTimeoutThreshold = 1000L;
private boolean doAcquireNanos(int arg,monospace;'>        throws InterruptedException {    // 若超時時間小於等於 0,直接獲取失敗    if (nanosTimeout <= 0L)        return false;    // 計算截止時間    final long deadline = System.nanoTime() + nanosTimeout;    final Node node = addWaiter(Node.EXCLUSIVE);                return true;            nanosTimeout = deadline - System.nanoTime();            // 已經超時了,獲取失敗            if (nanosTimeout <= 0L)                return false;            // 若大於自旋時間,則執行緒休眠;否則自旋                nanosTimeout > spinForTimeoutThreshold)                LockSupport.parkNanos(this,monospace;'>            // 若被中斷,則響應            if (Thread.interrupted())public final boolean release(int arg) {    // 嘗試釋放資源,若成功則返回 true    if (tryRelease(arg)) {        Node h = head;        // 若頭節點不為空,且等待狀態不為 0(此時為 SIGNAL)        // 則喚醒其後繼節點        if (h != null && h.waitStatus != 0)            unparkSuccessor(h);}

與 tryAcquire 方法類似,tryRelease 方法在 AQS 中也是丟擲異常,同樣交由子類實現:

protected boolean tryRelease(int arg) {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,0);     * 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) {        // 若後繼節點是取消狀態,則從尾節點向前遍歷,找到 node 節點後面一個未取消狀態的節點        s = null;        for (Node t = tail; t != null && t != node; t = t.prev)            if (t.waitStatus <= 0)                s = t;    // 喚醒node節點的後繼節點    if (s != null)        LockSupport.unpark(s.thread);private void cancelAcquire(Node node) {    // Ignore if node doesn't exist    if (node == null)        return;            node.thread = null;        // Skip cancelled predecessors    // 跳過取消狀態的前驅節點    Node pred = node.prev;    while (pred.waitStatus > 0)        node.prev = pred = pred.prev;    // predNext is the apparent node to unsplice. CASes below will    // fail if not,in which case,we lost race vs another cancel    // or signal,so no further action is necessary.    // 前驅節點的後繼節點引用    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;    // If we are the tail,remove ourselves.    // 若該節點為尾節點(後面沒其他節點了),將 predNext 指向 null    if (node == tail && compareAndSetTail(node,pred)) {        compareAndSetNext(pred,predNext,null);        




int ws;
       if (pred != head &&            ((ws = pred.waitStatus) == Node.SIGNAL ||             (ws <= 0 && compareAndSetWaitStatus(pred,Node.SIGNAL))) &&            pred.thread != null) {            Node next = node.next;            if (next != null && next.waitStatus <= 0)                compareAndSetNext(pred,next);            // 前驅節點為頭節點,表明當前節點為第一個,取消時喚醒它的下一個節點            unparkSuccessor(node);        node.next = node; // help GC}

該方法的主要操作:

1. 將 node 節點設定為取消(CANCELLED)狀態;

2. 找到它在佇列中非取消狀態的前驅節點 pred:

    2.1 若 node 節點是尾節點,則前驅節點的後繼設為空,

    2.2 若 pred 不是頭節點,且狀態為 SIGNAL,則後繼節點設為 node 的後繼節點;

    2.3 若 pred 是頭節點,則喚醒 node 的後繼節點。


PS: 該過程可以跟雙連結串列刪除一個節點的過程進行對比分析。

小結

本文分析了以獨佔模式獲取資源的三種方式,以及釋放資源的操作。分別為:


1. acquire: 獨佔模式獲取資源,忽略中斷;

2. acquireInterruptibly: 獨佔模式獲取資源,響應中斷;

3. tryAcquireNanos: 獨佔模式獲取資源,響應中斷,有超時;

4. release: 釋放資源,喚醒主佇列中的下一個執行緒。


這幾個方法都可以類比 Lock 介面的相關方法定義。

相關閱讀:

JDK原始碼分析-AbstractQueuedSynchronizer(1)

JDK原始碼分析-Lock&Condition


Stay hungry,stay foolish.