原始碼|併發一枝花之ReentrantLock與AQS(3):Condition

monkeysayhi發表於2018-01-01

ReentrantLock#lock()、ReentrantLock#unlock()、ReentrantLock#lockInterruptibly()的分析見前文:

本文是《原始碼|併發一枝花之ReentrantLock與AQS》系列的最後一篇,分析與顯式鎖ReentrantLock配套的顯式條件佇列AQS#ConditionObject,以完善對AQS的使用和理解。

JDK版本:oracle java 1.8.0_102

介面宣告

public interface Lock {
    ...
    Condition newCondition();
    ...
}
public interface Condition {
    ...
    void await() throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    void signal();

    void signalAll();
    ...
}
複製程式碼

Lock介面對標內建鎖,而Condition介面對標內建條件佇列。Condition主要提供了await、signal兩種語義,和兩種語義的衍生品。

語義、用法

對內建條件佇列語義、用法的分析可參考以下兩篇文章,其同時適用於本文的顯式條件佇列:

實現原理

直接使用了AQS的內部類ConditionObject。

ReentrantLock中提供的條件佇列不能單獨設定公平策略,只能在建立鎖時指定。因此,我們仍舊以預設的非公平策略為例進行分析。

newCondition

    public Condition newCondition() {
        return sync.newCondition();
    }
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
        ...
    }
複製程式碼

ConditionObject

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        public ConditionObject() { }
        ...
    }
    ...
}
複製程式碼

ConditionObject實現了Condition介面,提供了一個空構造器。

接下來,我們依次分析ConditionObject的await()方法和signal()方法,最後送一個signalAll()。

await

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        public final void await() throws InterruptedException {
            // 主動檢查中斷,及時丟擲InterruptedException。
            if (Thread.interrupted())
                throw new InterruptedException();
            // 建立等待節點並放入隊尾
            Node node = addConditionWaiter();
            // 釋放鎖,並儲存釋放前的鎖狀態(ownerThread的重入次數)
            int savedState = fullyRelease(node);
            // 阻塞,直到收到訊號或被中斷
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 申請恢復鎖狀態,並更新中斷模式
            int interruptMode = 0; // 調整了一下位置,不影響
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // 移除已被取消的節點
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            // 如果之前發生了中斷,則根據中斷模式重放中斷
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
        ...
    }
    ...
}
複製程式碼

ConditionObject#await()同ReentrantLock#lockInterruptibly()一樣,都是可中斷的:呼叫ConditionObject#await()後,當前執行緒將保持阻塞,直到收到訊號或被中斷。

  1. 主動檢查中斷,及時丟擲InterruptedException。
  • 建立等待節點並放入隊尾
  • 釋放鎖,並儲存釋放前的鎖狀態(ownerThread的重入次數)
  • 阻塞,直到收到訊號或被中斷
  • 重新申請鎖,並更新中斷模式
  • 移除已被取消的節點
  • 如果之前發生了中斷,則根據中斷模式重放中斷

下面詳細分析。

addConditionWaiter

檢查中斷後,建立等待節點並放入隊尾:

            Node node = addConditionWaiter();
複製程式碼
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }
        ...
    }
    ...
}
複製程式碼

lastWaiter初始化為null,至此,我們還未修改過lastWaiter,則lastWaiter仍為null。

從而,執行8行後t為null。跳到14行,建立CONDITION==-2狀態的節點。然後15-19行,將firstWaiterlastWaiter都指向新節點node,初始化條件佇列自己的等待佇列。這在效能上是必要的:如此,我們可以在一個顯式鎖上建立多個顯式條件佇列,更容易減少無效競爭,在保證正確性的前提下設計高效能的併發程式。

需要提醒一下,儘管此處沒有任何執行緒安全的保護,但實際使用時不會出現任何執行緒安全問題——因為條件佇列的使用要求我們在呼叫await或signal時持有與該條件佇列唯一相關的鎖

到這裡,得到兩個ReentrantLock與ConditionObject在實現上的重要區別:

  • ReentrantLock建立的節點,初始狀態為0;而ConditionObject建立的節點,初始狀態為CONDITION==-2
  • ReentrantLock使用AQS內建的等待佇列,由AQS維護;而每個ConditionObject都維護自己的等待佇列。

fullyRelease

釋放鎖,並儲存釋放前的鎖狀態(ownerThread的重入次數):

            int savedState = fullyRelease(node);
複製程式碼
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }
    ...
}
複製程式碼

8行儲存鎖狀態。9-14行嘗試狀態轉換savedState->0完全釋放鎖,並返回鎖狀態。只要狀態正常,AQS#release()在此處一定會返回true,因此,如果返回false可直接丟擲IllegalMonitorStateException。

回憶:

  • ReentrantLock#unlock()中分析了AQS#release()。
  • ReentrantLock#lock()中分析了try-finally的取消框架。

AQS#fullyRelease()返回後,呼叫ConditionObject#await()的執行緒就放棄了鎖,後面截止到重新獲取鎖前,只能使用開放呼叫。

開放呼叫:Open Call,呼叫某個方法時不需要持有鎖(可能方法內部實現需要鎖,但外部呼叫不需要)。

isOnSyncQueue

接下來,幾個方法配套完成“阻塞,直到收到訊號或被中斷”:

            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
複製程式碼

首先是AQS#isOnSyncQueue():

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;

        return findNodeFromTail(node);
    }
    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }
    ...
}
複製程式碼

在建立節點node後未修改node.waitStatus,因此,仍滿足node.waitStatus == Node.CONDITION,直接返回false,表示當前節點未阻塞。

當然,此時也滿足node.prev == nullnode.next == null

回到ConditionObject#await(),會進入while迴圈,然後當前執行緒陷入阻塞。

至此,呼叫ConditionObject#await()的執行緒已陷入阻塞,對ConditionObject#await()的分析告一段落。要暫時先跳至ConditionObject#signal()。


ConditionObject#signal()返回之後,node已經被放入了AQS內部的等待佇列。此時,呼叫ConditionObject#await()的執行緒可能因前繼節點被取消而提前喚醒,也可能等待呼叫ConditionObject#signal()的執行緒unlock之後才能被喚醒,還有可能因被中斷而被喚醒。最容易被忽略的是,還可能在收到訊號後未喚醒,而在等待解鎖喚醒的時候被中斷

前兩種喚醒都可被認為是“訊號喚醒”,第三種是“訊號前中斷喚醒”,第四種是“訊號後中斷喚醒”。因此,在回到ConditionObject#await()後,需要進入_18-19行判斷是否發生了中斷和發生中斷的時機_。

插播分析完isOnSyncQueue

在繼續走邏輯前,先插播分析完AQS#isOnSyncQueue(),後面要多次用到這個方法。

AQS#isOnSyncQueue()判斷節點node是否已經被放入了AQS內部的等待佇列,是的話返回true,否則返回false。主要分幾種情況:

  • 如果node.waitStatus == CONDITION,則一定未放入。因為AQS#transferForSignal()6行還沒來得及執行。
  • 如果node.prev == null,則一定未放入。因為AQS#transferForSignal()6行執行完但9行未執行完。
  • 如果node.next != null,則一定已放入。因為已經有了後繼節點,則node本身肯定已經完成入隊(ConditionObject內部的等待使用的後繼指標為nextWaiter)。
  • 否則,說明滿足node.waitStatus != CONDITION && node.prev != null && node.next == null,該狀態無法確定node處於“未放入”還是“已放入”的狀態。回憶AQS#enq()可知,node.prev != null時,可能正在嘗試CAS插入node,無法確定是在插入前還是插入後,也無法確定是否插入成功。AQS#findNodeFromTail()從尾節點開始遍歷,如果能夠遍歷到node,則一定已放入(當然,next方向不一定滿足一致性);否則,當前時刻還未插入或未插入成功,即一定未放入。

回憶:

  • AQS#enq()插入新節點的過程。
  • AQS.shouldParkAfterFailedAcquire()移除CACELLED節點的過程。

從兩個過程可知,AQS內部的等待佇列是一個弱一致性雙向連結串列,其插入節點和移除取消節點的過程不保證next方向的一致性。

checkInterruptWhileWaiting

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }
        ...
    }
    ...
    final boolean transferAfterCancelledWait(Node node) {
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            return true;
        }
        
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }
    ...
}
複製程式碼

首先,根據Thread.interrupted()判斷是否發生了中斷。如果未發生中斷,則Thread.interrupted()返回false,ConditionObject#checkInterruptWhileWaiting()最終返回0

否則,繼續呼叫AQS#transferAfterCancelledWait()判斷髮生中斷的時機。如果是在收到訊號前發生了中斷,AQS#transferForSignal()6行還沒來得及執行,必然滿足node.waitStatus == CONDITION,則CAS設定node.waitStatus並將node入隊(等待後面呼叫AQS#acquireQueued()競爭鎖),然後返回true,ConditionObject#checkInterruptWhileWaiting()最終返回THROW_IE == -1

否則,一定是在收到訊號後發生了中斷,但可能AQS#transferForSignal()6行執行完但9行未執行完,即node未完成入隊,因此,21-22行空等待至node完成入隊,然後返回false,ConditionObject#checkInterruptWhileWaiting()最終返回REINTERRUPT == 1

回到ConditionObject#await()後:

  • 如果ConditionObject#checkInterruptWhileWaiting()返回0,表示未發生中斷,屬於訊號喚醒,且已放入AQS內部等待佇列,則退出迴圈。
  • 如果ConditionObject#checkInterruptWhileWaiting()返回非0,表示發生了中斷,則記錄中斷模式interruptMode,以區分訊號前中斷喚醒與訊號後中斷喚醒,然後break跳出迴圈。

猴子沒想明白一點:

使用while是為了防止“訊號喚醒後node未放入AQS內部等待佇列”的情況,但什麼時候會出現該情況呢?看起來只有發生中斷的時候,但是如果發生中斷,則一定會break退出迴圈,也不需要回到while判斷。

現在終於跳出了迴圈,要重新申請鎖了。

訊號前中斷的特殊情況

訊號前中斷會導致node同時處於AQS與ConditionObject兩方的等待佇列中(使用不同的指標連線節點):

image.png

而另外兩種情況下,節點都被遷入了AQS內部等待佇列。

因此,只要ConditionObject內部等待佇列中的節點滿足node.waitStatus == SIGNALnode.waitStatus == 0,就可以判斷其同時位於於AQS與ConditionObject兩方的等待佇列中,也就能斷定該節點屬於訊號前中斷喚醒。

訊號前中斷喚醒的節點是無效的,需要被清理,可以用該條件找出這部分節點。該結論將在分析ConditionObject#unlinkCancelledWaiters()時派上用場。

acquireQueued

申請恢復鎖狀態,並更新中斷模式:

            int interruptMode = 0; // 調整了一下位置,不影響
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
複製程式碼

lock過程分析了AQS#acquireQueued(),該方法會阻塞,直到申請到鎖,並返回中斷標誌。

如果申請鎖的過程被中斷,且之前等待時並不是訊號前中斷喚醒(即,訊號喚醒或訊號後中斷喚醒),就將interruptMode置為REINTERRUPT。

AQS#acquireQueued()返回後,呼叫ConditionObject#await()的執行緒就重新獲得了鎖,以後就可以使用非開放呼叫了。

unlinkCancelledWaiters

移除已被取消的節點:

            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
複製程式碼

實際上,ConditionObject#unlinkCancelledWaiters()用於清理ConditionObject內部等待佇列中的非CONDITION節點。具體來說:

  • 如果ConditionObject#await()時(更確切的說,AQS#fullyRelease()完全釋放鎖時)失敗,節點轉為CANCELLED狀態,需要被清理。
  • 分析AQS#transferAfterCancelledWait()有一個結論,訊號前中斷會導致node同時處於AQS與ConditionObject兩方的等待佇列中(使用不同的指標連線節點)。這些節點將隨著AQS#acquireQueued()的執行轉為SIGNAL或0狀態。但由於ConditionObject#await()在收到訊號前(更確切的說,在AQS#fullyRelease()完全釋放鎖後、收到訊號前)被中斷,因此上述節點也是無效的,需要被清理。

而被訊號喚醒或訊號後中斷喚醒的節點,將首先移出ConditionObject內部等待佇列,再進行狀態轉換。

綜上,只需要清理ConditionObject內部等待佇列中的非CONDITION節點。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
            while (t != null) {
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }
        ...
    }
    ...
}
複製程式碼

我發現大神們都喜歡寫這種功能簡單效能高,但乍一看肯定看不懂的程式碼。。。

從firstWaiter開始,trail記錄上一個CONDITION節點,t為當前節點;如果當前節點t滿足t.waitStatus != Node.CONDITION,就移除;遍歷到隊尾的時候,如果隊尾也是非CONDITION節點,則更新lastWaiter。

注意,ConditionObject#unlinkCancelledWaiters()不是執行緒安全的,所以呼叫時必須持有鎖。下面分析的兩個清理時機都是持有鎖的。

觸發清理的時機

猴子分析的第二種時機可能有問題。以下分析僅供參考,歡迎指正。

對應的,需要在兩種情況下觸發清理:

  • 如果ConditionObject#await()時失敗,則該節點必為隊尾節點,因此,在下一次插入節點前,檢查隊尾節點的狀態即可。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            ...
        }
        ...
    }
    ...
}
複製程式碼
  • 如果是訊號前中斷,則該節點可能位於佇列的任意位置。因此,如果發生了中斷就需要清理。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        public final void await() throws InterruptedException {
            ...
            // 阻塞,直到收到訊號或被中斷
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 申請恢復鎖狀態,並更新中斷模式
            int interruptMode = 0; // 調整了一下位置,不影響
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // 移除已被取消的節點
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            ...
        }
        ...
    }
    ...
}
複製程式碼

node.nextWaiter != null與中斷的判斷是沒有關係的。頂多可以認為這裡沒有判斷中斷,至於條件node.nextWaiter != null僅為了將尾節點判斷併入對第一種時機的處理。

reportInterruptAfterWait

最後,如果之前發生了中斷,則根據中斷模式重放中斷:

            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
複製程式碼

只有確實發生了中斷時,才會呼叫ConditionObject#reportInterruptAfterWait()。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }
        ...
    }
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
    ...
}
複製程式碼

簡單根據中斷模式interruptMode決定如何重放:

  • 如果滿足interruptMode == THROW_IE,則丟擲InterruptedException
  • 如果滿足interruptMode == REINTERRUPT,則重置中斷標誌。
  • 否則,什麼都不做(儘管目前的分析不會觸發該分支)。
InterruptedException 和 Thread#interrupt() 的區別
  • 如果丟擲InterruptedException,則外界必須處理(捕獲或繼續外拋)。
  • 如果呼叫Thread#interrupt(),則僅設定中斷標誌。JDK提供的某些阻塞方法會處理該標誌,詳見Javadoc;但使用者自己實現的方法,是否會處理該標誌,處理是否及時,都無法做出保證。

更詳細的區別暫時還不清楚,因此,也不明白這裡要區分中斷模式。

不過,關於JVM的中斷機制,猴子終有一天會整明白的!!!

await小結

由於await與signal配合時才能發揮作用,分析二者時也要一起分析。本節從await切入,在await執行緒阻塞後轉而分析signal執行緒,最後在再回到await執行緒繼續分析。這部分原始碼的條件和結論非常多,程式碼耦合也比較大,雖然大大犧牲了可讀性,不過作為JDK,換來的效能提高是居功至偉的。

本文也留下了幾個問題,猴子暫時還沒有解決。如果有朋友能指點一二,猴子感激不盡。

signal

signal與await也是對偶的。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
        ...
    }
    ...
}
複製程式碼

8-9行的判斷有兩個意義:

  • 確定目前持有鎖,因為條件佇列的使用要求我們在呼叫await或signal時持有與該條件佇列唯一相關的鎖
  • 確定目前AQS的狀態是獨佔的,否則不能配套使用鎖和條件佇列。

ReentrantLock是獨佔的,假設目前持有鎖,則跳至10-12行。如果firstWaiter不為null,則表示有節點進入了條件佇列內部的等待佇列,需要被喚醒。按照FIFO的順序進行喚醒,所以每次喚醒的都是頭節點。

剛才我們呼叫ConditionObject#await()加入了一個節點,接下來要喚醒該節點。

doSignal

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
        ...
    }
    ...
}
複製程式碼

9-11行使firstWaiter後移一位,也就是移除需要被喚醒的fisrt節點。12-13行呼叫AQS#transferForSignal(),嘗試將條件佇列內部的等待節點轉換為AQS內部的等待節點,如果當前節點轉換失敗,就繼續嘗試下一節點,直至成功或遍歷到隊尾。

transferForSignal
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    final boolean transferForSignal(Node node) {
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
    ...
}
複製程式碼

6-7行嘗試CAS修改node.waitStatus。與ReentrantLock#look()類似,如果ConditionObject#await()阻塞獲取的過程失敗,失敗的當時只會將新節點的狀態標記為CACELLED==1。因此,如果19行CAS設定失敗的,此時一定滿足node.waitStatus==CACELLED,則可直接返回false,表示轉換失敗。否則,表示可以轉換,繼續執行。

9行呼叫AQS#enq()入隊,返回舊的隊尾節點,也就是新節點node的前繼節點。

首先要再次敲黑板:條件佇列的使用要求我們在呼叫await或signal時持有與該條件佇列唯一相關的鎖。同時,ReentrantLock#unlock()的執行也要求先持有鎖。因此,只有正在執行ReentrantLock#lock()的執行緒能夠與正在執行ConditionObject#signal()的執行緒發生競爭。更確切的說,只有正在執行AQS#acquireQueued()的執行緒T1能夠與正在執行AQS#transferForSignal()10-12行的執行緒T2發生競爭

10-12行的迷惑性就來源於此。實際上,就算去掉10-12行也是滿足正確性要求的。因為執行緒T2釋放鎖後,依然會將從隊頭開始的第一個非取消節點喚醒,該節點會繼續ConditionObject#await()中的工作(稍後回去分析)。10-12行是為了進一步提升效能,針對兩種情況:

  • 如果插入node前,AQS內部等待佇列的隊尾節點就已經被取消,則滿足wc > 0
  • 如果插入node後,AQS內部等待佇列的隊尾節點已經穩定,滿足tail.waitStatus == 0,但在執行ws > 0之後!compareAndSetWaitStatus(p, ws, Node.SIGNAL)之前被取消,則CAS也會失敗,滿足compareAndSetWaitStatus(p, ws, Node.SIGNAL) == false

這兩種情況下,提前喚醒node能夠在等待鎖的同時,預先完成一部分ConditionObject#await()中無需同步的工作。這部分成本不能被輕易忽視,因為條件佇列被應用最多的場景是高併發,大量執行緒累加起來的成本是很可觀的。

最後,AQS#transferForSignal()一定會返回true,表示node已經插入AQS內部的等待佇列,而不關心其此時是否被喚醒。

回到ConditionObject#doSignal()與ConditionObject#signal(),成功結束。對於使用者而言,執行緒此時已經被喚醒,儘管其大概率還在AQS內部阻塞排隊,等待重新獲得鎖。

現在,可以跳回ConditionObject#await()繼續分析了。之前分析到了16行的AQS#isOnSyncQueue()方法。


signal沒有小結

signalAll

最後送一個signalAll():

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }
        ...
    }
    ...
}
複製程式碼

12行前的邏輯與ConditionObject#signal()相同。

doSignalAll

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public class ConditionObject implements Condition, java.io.Serializable {
        ...
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }
        ...
    }
    ...
}
複製程式碼

8行一次性移除所有節點。9-14行,利用first仍持有的隊首節點,遍歷佇列依次轉換所有節點。後同ConditionObject#signal()。

總結

本文分析了顯式條件佇列Condition。AQS的內部類ConditionObject實現了Condition介面,內部維護自己的等待佇列,加上與AQS的互動,實現了條件佇列的基本語義。

與Lock#lock()相似,ConditionObject#await()也有諸多衍生品,如ConditionObject#awaitUninterruptibly()、ConditionObject#awaitNanos()等,感興趣的朋友可以仿照Lock#lockInterruptibly()的思路進行分析。


本文連結:原始碼|併發一枝花之ReentrantLock與AQS(3):Condition
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章