CyclicBarrier多任務協同的利器

疫情逐漸好轉,部門也有半年多沒有TB團建了,並且金三銀四,部門又招了一波新人;
leader讓你組織一次TB:週六上午,大家先到公司集合,然後一起去朝陽公園玩,最後一起去餐廳聚餐,然後回家。
爲了體現團隊集體意識,在每次開啓新項目時,需要所有人一起開始行動(不能早來的人都把東西喫光了吧~),並且每個階段活動完成後,需要統計人數、向上彙報。

這個場景,如何藉助JUC併發工具來實現呢?
我們先來梳理一下,任務特點:

  • 很顯然,每次開啓新項目時,需要所有人一起開始行動 這是個多任務相互等待,直到所有人都到達一個點時,纔開始執行;
  • 同時,TB活動是分爲多個階段的,每個階段都有具體要做的事;
  • 每個階段完後,組織者還得做點而外的事
  • 參與者的數量,是確定的

看到多任務相互等待,相信很多人已經想到了 CyclicBarrier。

沒錯,這個TB任務的特點,其實也是使用 CyclicBarrier 時的特點。
下面來看 如何使用 CyclicBarrier 實現TB成員管理的。

先來看看 CyclicBarrier 的源碼註釋;

A synchronizati on aid that allows a set of threads to all wait for each other to reach a common barrier point.

描述如下:多個線程相互等待,直到所有線程到達同一個同步點,再繼續一起執行。

CyclicBarrier適用於多個線程有固定的多步需要執行,線程間互相等待,當都執行完了,在一起執行下一步。

CyclicBarrier 字面意思迴環柵欄,通過它可以實現讓一組線程等待至某個狀態之後再全部同時執行。

  • 叫做迴環,是因爲當所有等待線程都被釋放以後,CyclicBarrier可以被重用。
  • 叫做柵欄,大概是描述所有線程被柵欄擋住了,當都達到時,一起跳過柵欄執行,也算形象。我們可以把這個狀態就叫做barrier。

CyclicBarrier 的API

public CyclicBarrier(int parties)
public int await()

構造函數,指定參與者數量;
await()讓線程阻塞在柵欄。

CyclicBarrier實現TB成員協同

我們用 parties 變量指定了參與者數量;用sleep(隨機數)來模擬每個TB活動不同成員的耗時;代碼實現如下:

    static final Random random = new Random();
    static final CyclicBarrier cyclicBarrier = new CyclicBarrier(parties);


    static class StaffThread extends Thread {
        @Override
        public void run() {
            try {
                String staff = "員工【" + Thread.currentThread().getName() + "】";

                // 第一階段:來公司集合
                System.out.println(staff + "從家出發了……");
                Thread.sleep(random.nextInt(5000));
                System.out.println(staff + "到達公司");

                // 協同,第一次等大家到齊
                cyclicBarrier.await();

                // 第二階段:出發去公園
                System.out.println(staff + "出發去公園玩");
                Thread.sleep(random.nextInt(5000));
                System.out.println(staff + "到達公園門口集合");

                // 協同:第二次等大家到齊
                cyclicBarrier.await();

                // 第三階段:去餐廳
                System.out.println(staff + "出發去餐廳");
                Thread.sleep(random.nextInt(5000));
                System.out.println(staff + "到達餐廳");

                // 協同:第三次等大家到齊
                cyclicBarrier.await();

                // 第四階段:就餐
                System.out.println(staff + "開始用餐");
                Thread.sleep(random.nextInt(5000));
                System.out.println(staff + "用餐結束,回家");

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {

        // 所有人,開始行動
        for (int i = 0; i < parties; i++) {
            new StaffThread().start();
        }

    }

我們用 StaffThread 代表每個員工參加TB活動要做的事;每個成員在各個階段,花費的時間可能不同;
每個員工,在執行完成當前階段後 cyclicBarrier.await()進行阻塞(任務協同),等待大家到齊了再進入下一階段。

值得一提的是,CyclicBarrier 的計數器有自動重置的功能,當減到 0 的時候,會自動重置你設置的初始值,自動復原。這個功能用起來實在是太方便了。

由於 CyclicBarrier 的可重用特性,當所有等待線程都被釋放以後,CyclicBarrier可以被重用;
因此只要每個階段,所有成員都完成後,CyclicBarrier就會自動重用,以此往復。

看上去,很完美。就差一點了 —— TB組織者,要在每個階段結束後向上彙報;
這就用到了 CyclicBarrier 的回調函數功能,CyclicBarrier 的第二個構造方法:

  CyclicBarrier(int parties, Runnable barrierAction);

barrierAction 可以指定一個善後處理的task,在所有人都到達屏障點時,來執行;
就好比團建時,所有人都到達公園門口了,這時組織者喊“都別走,先拍個照”,然後橫幅一拉……
(嗚嗚嗚……)

下面來看 如何實現;
由於構造函數中,只能指定一個Runnable善後任務,但我們的TB活動有多個階段,每個階段都需要彙報一次,因此我們實現的Runnable任務,需要判斷在不同的階段,做不同的彙報;

我們用 peroid 變量代表當前階段,初始值爲1;
CyclicBarrier的可複用功能,在所有人都達到集合點後,執行一次 milestoneRunnable 善後任務,意味着 milestoneRunnable 執行一次後,就代表進入下一階段,因此peroid++;

    //階段
    static int peroid = 1;

    /**
     * 里程碑
     * 每階段完成後,會執行這裏;
     */
    static Runnable milestoneRunnable = new Runnable() {
        @Override
        public void run() {
            switch (peroid) {
                case 1:
                    System.out.println("********第1階段***************");
                    break;
                case 2:
                    System.out.println("********第2階段***************");
                    break;
                case 3:
                    System.out.println("********第3階段***************");
                    break;
            }
            peroid++;
        }
    };

我們換用帶回調函數的構造方法,再執行

static final CyclicBarrier cyclicBarrier = new CyclicBarrier(parties, milestoneRunnable);

public static void main(String[] args) {
    // 所有人,開始行動
    for (int i = 0; i < parties; i++) {
        new StaffThread().start();
    }

}

運行結果:

員工【Thread-0】從家出發了……
員工【Thread-2】從家出發了……
員工【Thread-1】從家出發了……
員工【Thread-2】到達公司
員工【Thread-0】到達公司
員工【Thread-1】到達公司
********第1階段***************
員工【Thread-1】出發去公園玩
員工【Thread-2】出發去公園玩
員工【Thread-0】出發去公園玩
員工【Thread-2】到達公園門口集合
員工【Thread-1】到達公園門口集合
員工【Thread-0】到達公園門口集合
********第2階段***************
員工【Thread-0】出發去餐廳
員工【Thread-2】出發去餐廳
員工【Thread-1】出發去餐廳
員工【Thread-0】到達餐廳
員工【Thread-1】到達餐廳
員工【Thread-2】到達餐廳
********第3階段***************
員工【Thread-2】開始用餐
員工【Thread-0】開始用餐
員工【Thread-1】開始用餐
員工【Thread-2】用餐結束,回家
員工【Thread-0】用餐結束,回家
員工【Thread-1】用餐結束,回家

通過這個例子,對 CyclicBarrier 的基本使用,是不是清晰了很多。

關於CyclicBarrier的回調函數

CyclicBarrier 的回調函數,可以指定一個線程池來運行,相當於異步完成;
如果不指定線程池,默認在最後一個執行await()的線程執行,相當於同步完成。

這有什麼區別呢?
實則關注性能的場景,區別很大。

CountDownLatch和CyclicBarrier讓多線程步調一致 文中的例子,對賬系統每天會校驗是否存在異常訂單,簡易實現如下:

while(存在未對賬訂單){
  // 查詢未對賬訂單
  pos = getPOrders();
  // 查詢派送單
  dos = getDOrders();
  // 執行對賬操作
  diff = check(pos, dos);
  // 差異寫入差異庫
  save(diff);
}

文中有使用CyclicBarrier的實現:查詢訂單和查詢派單 兩個線程相互等待,都完成時執行回調:進行check對賬

對賬任務可以大致分爲兩步,第一步查詢需要覈對的訂單、賬單;第二步執行check()、記錄差異。
兩個查詢操作可以併發完成,第二步執行check()、記錄差異可以看作是第一階段任務完成後的“結果彙總”,可以使用CyclicBarrier的回調函數來完成;
最終達到的效果圖:

也就是併發去查詢,查詢的結果讓 回調函數異步執行,好處是查詢線程直接進入下一階段、繼續查詢下一組數據;
中間使用一個同步容器,保存訂單數據即可。詳細思路&代碼,見:CountDownLatch和CyclicBarrier讓多線程步調一致

試想,假設讓CyclicBarrier的回調函數執行在一個回合裏最後執行await()的線程上,而且同步調用回調函數check(),調用完check之後,纔會開始第二回合。
所以check如果不另開一線程異步執行,就起不到性能優化的作用了。

CyclicBarrier 的回調函數究竟是哪個線程執行的呢?

如果你分析源碼,你會發現執行回調函數的線程是將 CyclicBarrier 內部計數器減到 0 的那個線程。
這裏強調一下:當看到回調函數的時候,一定問一問執行回調函數的線程是誰。
如果CyclicBarrier回調函數不使用隔離的線程池,則CyclicBarrier最後一個線程忙着執行回調,其他線程還在阻塞,可能適得其反。

問題思考:需求升級

需求升級了:實際TB活動中,可能有人白天有事,不能參加公園的活動、但晚上會來聚餐;有人白天能參加,晚上不能參加;
並且公園的門票,聚餐費用,因參與人數不同,又有不同。
思考:需求升級後,如何實現?CyclicBarrier 能完成嗎?

~~下回揭曉


推薦閱讀

本文首發於 公衆號 架構道與術(ToBeArchitecturer),歡迎關注、學習更多幹貨~

推薦閱讀
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章