併發程式設計之 CyclicBarrier 原始碼分析

莫那·魯道發表於2018-04-30

前言

在之前的介紹 CountDownLatch 的文章中,CountDown 可以實現多個執行緒協調,在所有指定執行緒完成後,主執行緒才執行任務。

但是,CountDownLatch 有個缺陷,這點 JDK 的文件中也說了:他只能使用一次。在有些場合,似乎有些浪費,需要不停的建立 CountDownLatch 例項,JDK 在 CountDownLatch 的文件中向我們介紹了 CyclicBarrier——迴圈柵欄。具體使用參見文章 併發程式設計之 執行緒協作工具類

原始碼分析

該類結構如下:

image.png

有一個我們常用的方法 await,還有一個內部類,Generation ,僅有一個引數,有什麼作用呢?

在 CyclicBarrier 中,有一個 “代” 的概念,因為 CyclicBarrier 是可以複用的,那麼每次所有的執行緒通過了柵欄,就表示一代過去了,就像我們的新年一樣。當所有人跨過了元旦,日曆就更新了。

為什麼需要這個呢?後面我們看原始碼的時候在細說,現在說有點不太容易懂。

再看看構造方法,有 2 個構造方法:

public CyclicBarrier(int parties) {
    this(parties, null);
}

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}
複製程式碼

如果使用 CyclicBarrier 就知道了,CyclicBarrier 支援在所有執行緒通過柵欄的時候,執行一個執行緒的任務。

parties 屬性就是執行緒的數量,這個數量用來控制什麼時候釋放開啟柵欄,讓所有線 程通過。

好了,CyclicBarrier 的最重要的方法就是 await 方法,當執行了這樣一個方法,就像是樹立了一個柵欄,將執行緒擋住了,只有所有的執行緒都到了這個柵欄上,柵欄才會開啟。

看看這個方法的實現。

await 方法實現

程式碼加註釋如下:

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    final ReentrantLock lock = this.lock;
    // 鎖住
    lock.lock();
    try {
        // 當前代
        final Generation g = generation;
        // 如果這代損壞了,丟擲異常
        if (g.broken)
            throw new BrokenBarrierException();

        // 如果執行緒中斷了,丟擲異常
        if (Thread.interrupted()) {
            // 將損壞狀態設定為 true
            // 並通知其他阻塞在此柵欄上的執行緒
            breakBarrier();
            throw new InterruptedException();
        }
        // 獲取下標    
        int index = --count;
        // 如果是 0 ,說明到頭了
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                // 執行柵欄任務
                if (command != null)
                    command.run();
                ranAction = true;
                // 更新一代,將 count 重置,將 generation 重置.
                // 喚醒之前等待的執行緒
                nextGeneration();
                // 結束
                return 0;
            } finally {
                // 如果執行柵欄任務的時候失敗了,就將柵欄失效
                if (!ranAction)
                    breakBarrier();
            }
        }

        for (;;) {
            try {
                // 如果沒有時間限制,則直接等待,直到被喚醒
                if (!timed)
                    trip.await();
                // 如果有時間限制,則等待指定時間
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                // g == generation >> 當前代
                // ! g.broken >>> 沒有損壞
                if (g == generation && ! g.broken) {
                    // 讓柵欄失效
                    breakBarrier();
                    throw ie;
                } else {
                    // 上面條件不滿足,說明這個執行緒不是這代的.
                    // 就不會影響當前這代柵欄執行邏輯.所以,就打個標記就好了
                    Thread.currentThread().interrupt();
                }
            }
            // 當有任何一個執行緒中斷了,會呼叫 breakBarrier 方法.
            // 就會喚醒其他的執行緒,其他執行緒醒來後,也要丟擲異常
            if (g.broken)
                throw new BrokenBarrierException();
            // g != generation >>> 正常換代了
            // 一切正常,返回當前執行緒所在柵欄的下標
            // 如果 g == generation,說明還沒有換代,那為什麼會醒了?
            // 因為一個執行緒可以使用多個柵欄,當別的柵欄喚醒了這個執行緒,就會走到這裡,所以需要判斷是否是當前代。
            // 正是因為這個原因,才需要 generation 來保證正確。
            if (g != generation)
                return index;
            // 如果有時間限制,且時間小於等於0,銷燬柵欄,並丟擲異常
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}
複製程式碼

程式碼雖然長,但整體邏輯還是很簡單的。總結一下該方法吧。

  1. 首先,每個 CyclicBarrier 都有一個 Lock,想執行 await 方法,就必須獲得這把鎖。所以,CyclicBarrier 在併發情況下的效能是不高的。

  2. 一些執行緒中斷的判斷,注意,CyclicBarrier 中,只有有一個執行緒中斷了,其餘的執行緒也會丟擲中斷異常。並且,這個 CyclicBarrier 就不能再次使用了。

  3. 每次執行緒呼叫一次 await 方法,表示這個執行緒到了柵欄這裡了,那麼就將計數器減一。如果計數器到 0 了,表示這是這一代最後一個執行緒到達柵欄,就嘗試執行我們構造方法中輸入的任務。最後,將代更新,計數器重置,並喚醒所有之前等待在柵欄上的執行緒。

  4. 如果不是最後一個執行緒到達柵欄了,就使用 Condition 的 await 方法阻塞執行緒。如果等待過程中,執行緒中斷了,就丟擲異常。這裡,注意一下,如果中斷的執行緒的使用 CyclicBarrier 不是這代的,比如,在最後一次執行緒執行 signalAll 後,並且更新了這個“代”物件。在這個區間,這個執行緒被中斷了,那麼,JDK 認為任務已經完成了,就不必在乎中斷了,只需要打個標記。所以,catch 裡的 else 判斷用於極少情況下出現的判斷——任務完成,“代” 更新了,突然出現了中斷。這個時候,CyclicBarrier 是不在乎的。因為任務已經完成了。

  5. 當有一個執行緒中斷了,也會喚醒其他執行緒,那麼就需要判斷 broken 狀態。

  6. 如果這個執行緒被其他的 CyclicBarrier 喚醒了,那麼 g 肯定等於 generation,這個事件就不能 return 了,而是繼續迴圈阻塞。反之,如果是當前 CyclicBarrier 喚醒的,就返回執行緒在 CyclicBarrier 的下標。完成了一次衝過柵欄的過程。

總結

從 await 方法看,CyclicBarrier 還是比較簡單的,JDK 的思路就是:設定一個計數器,執行緒每呼叫一次計數器,就減一,並使用 Condition 阻塞執行緒。當計數器是0的時候,就喚醒所有執行緒,並嘗試執行建構函式中的任務。由於 CyclicBarrier 是可重複執行的,所以,就需要重置計數器。

CyclicBarrier 還有一個重要的點,就是 generation 的概念,由於每一個執行緒可以使用多個 CyclicBarrier,每個 CyclicBarrier 又都可以喚醒執行緒,那麼就需要用代來控制,如果代不匹配,就需要重新休眠。同時,這個代還記錄了執行緒的中斷狀態,如果任何執行緒中斷了,那麼所有的執行緒都會丟擲中斷異常,並且 CyclicBarrier 不再可用了。

總而言之,CyclicBarrier 是依靠一個計數器實現的,內部有一個 count 變數,每次呼叫都會減一。當一次完整的柵欄活動結束後,計數器重置,這樣,就可以重複利用了。

而他和 CountDownLatch 的區別在於,CountDownLatch 只能使用一次就 over 了,CyclicBarrier 能使用多次,可以說功能類似,CyclicBarrier 更強大一點。並且 CyclicBarrier 攜帶了一個在柵欄處可以執行的任務。更加靈活。

下面來一張圖,說說 CyclicBarrier 的流程。和 CountDownLatch 類似:

image.png

相關文章