Phaser多任務協同的神器

上篇 CyclicBarrier多任務協同的利器 我們藉助部門TB團建的例子,一步步分析了 CyclicBarrier 多線程協調的功能。
並在文章末尾,留出思考
實際部門TB活動中,可能有人白天有事,不能參加公園的活動、但晚上會來聚餐;有人白天能參加,晚上不能參加;
並且公園的門票,聚餐費用,因參與人數不同,又有不同,需要統計各階段的參與人數,以此計算經費。
需求升級後,如何實現呢?CyclicBarrier 能完成嗎?

其實在上篇文章中,我們分析了初版TB需求的任務特點,其中之一就是參與者的數量,是確定的
但當前需求,多個參與階段的參與者數量,各不相同,基本確定 CyclicBarrier 完成不了。
———— 別慌,針對多個階段,靈活設置參與者數量的場景,JDK提供了工具類 Phaser。

照舊,先看看 Phaser 的源碼註釋:

A reusable synchronization barrier, similar in functionality to
 * {@link java.util.concurrent.CyclicBarrier CyclicBarrier} and
 * {@link java.util.concurrent.CountDownLatch CountDownLatch}
 * but supporting more flexible usage.

Phaser 是一個可重用的同步屏障,功能上跟 CyclicBarrier 和 CountDownLatch 相似,但支持更多靈活的用法。

看過 CountDownLatch的兩種常用場景CyclicBarrier多任務協同的利器 的朋友,一定了解:CountDownLatch 能夠實現一個或多個線程阻塞等待,直到其他線程完成後再執行;
而 CyclicBarrier 允許多個線程相互等待,直到所有參與者到達屏障同步點後,再往下執行。
可以說,Phaser 是二者功能的增強和結合。

Phaser 階段協同器

Java 7 中增加的一個用於多階段同步控制的工具類,他包含了 CycIicBarrier 和 CountDownLatch 的相關功能,讓它們更強大靈活。
下面通過部門TB,多階段不同參與者的例子,具體探究 Phaser 的原理。

部門團建,需求升級

公司組織週末郊遊,大家各自從公司出發到公園集合,大家都到了之後,出發到公園各自遊玩,然後在公園門口集合,再去餐廳就餐,大家都到了就可以用餐,有的員工白天有事,選擇
參加晚上的聚餐,有的員工則晚上有事,只參加白天的活動。

任務特點分析:

  • 多階段協同,但階段的參與數是可變的,用 CyclicBarrier 好像不好實現。
  • 假定 第一階段:到公司集合5人(任務數5),去公園遊玩。
  • 第二階段:到公園門口集合,有2人因晚上有事,自行回家了;則3人去餐廳,這是減少參與數(任務數變爲3)
  • 第三階段:餐廳集合,有另4人蔘與聚餐,這是增加參與數(任務數變爲7)

實際上,當前任務最大的特點是:多階段等待一起出發、每階段的任務數可靈活調整。

多個線程協作執行的任務,分爲多個階段,每個階段都可以有任意個參與者線程,可以隨時註冊並參與到某個階段;
當一個階段中所有任務都完成之後,Phaser 的 onAdvance() 被調用(可以通過覆蓋添加自定文處理邏輯(類似CyclicBarrier循環屏障使用的Runnable接口)),然後Phaser釋放等待線程,自動進入下個階段,如此循環,直到Phaser不再包含任何參與者。

由於 Phaser 比較複雜,API也較爲繁多,下面將 Phaser 提供的API分爲多組。
構造方法

  • newPhaser() 不指定數量,參與任務數爲0。
  • new Phaser(int parties) 指定初始參與任務數
  • new Phaser(Phaser phaser) 指定父階段器,子對象整體作爲一個參與者加入到父對象,當子對象中沒有參與者時,自動從父對象解除註冊
  • new Phaser(Phaser phaser,int parties)

增減參與任務數方法

  • int register() 增加一個數,返回當前階段號。
  • int bulkRegister(int parties) 增加指定個數,返回當前階段號。
  • int arriveAndDeregister() 減少一個任務數,返回當前階段號。

到達、等待方法

  • int arrive() 到達(任務完成),返回當前階段號。
  • int arriveAndAwaitAdvance() 到達後等待其他任務到達,返回到達階段號。
  • int awaitAdvance(int phase) 在指定階段等待(必須是當前階段纔有效)
  • int awaitAdvanceInterruptibly(int phase) 階段到達觸發動作
  • int awaitAdvanceInterruptiBly(int phase,long timeout,TimeUnit unit)
  • protected boolean onAdvance(int phase,int registeredParties)類似CyclicBarrier的觸發命令,通過重寫該方法來增加階段到達動作,該方法返回true將終結Phaser對象。

Phaser其他API:

  • void forceTermination() 強制結束
  • int getPhase() 獲取當前階段號
  • boolean isTerminated() 判斷是否結束

注意事項:
單個 Phaser 實例允許的註冊任務數的.上限是65535,如果參與任務數超過,可以用父子Phaser樹的方式,通過父子關聯來增加參與者上限。

爲什麼是65535,這和 Phaser 的實現有關:
Phaser中的state狀態,64位的屬性state不同位被用來存放不同的值,低16位存放unarrived,低32位中的高16位存放parties,高32位的低31位存放phase,最高位存放terminated,即Phaser是否關閉;
2^16=65536

Phaser 實現多任務協同

下面來看,如何使用 Phaser 完成多階段任務協同。
我們首先將團建的不同階段任務,定義在 StaffTask :

static final Random random = new Random();

static class StaffTask {
    public void step1Task() throws InterruptedException {
        // 第一階段:來公司集合
        String staff = "員工【" + Thread.currentThread().getName() + "】";
        System.out.println(staff + "從家出發了……");
        Thread.sleep(random.nextInt(5000));
        System.out.println(staff + "到達公司");
    }
    
    public void step2Task() throws InterruptedException {
        // 第二階段:出發去公園
        String staff = "員工【" + Thread.currentThread().getName() + "】";
        System.out.println(staff + "出發去公園玩");
        Thread.sleep(random.nextInt(5000));
        System.out.println(staff + "到達公園門口集合");
    
    }
    
    public void step3Task() throws InterruptedException {
        // 第三階段:去餐廳
        String staff = "員工【" + Thread.currentThread().getName() + "】";
        System.out.println(staff + "出發去餐廳");
        Thread.sleep(random.nextInt(5000));
        System.out.println(staff + "到達餐廳");
    
    }
    
    public void step4Task() throws InterruptedException {
        // 第四階段:就餐
        String staff = "員工【" + Thread.currentThread().getName() + "】";
        System.out.println(staff + "開始用餐");
        Thread.sleep(random.nextInt(5000));
        System.out.println(staff + "用餐結束,回家");
    }
}

還是用隨機數,模擬不同參與者的耗時。

重點是下面的 main 方法:

public static void main(String[] args) {

    final Phaser phaser = new Phaser() {
        @Override
        protected boolean onAdvance(int phase, int registeredParties) {
            // 參與者數量,去除主線程
            int staffs = registeredParties - 1;
            switch (phase) {
                case 0:
                    System.out.println("大家都到公司了,出發去公園,人數:" + staffs);
                    break;
                case 1:
                    System.out.println("大家都到公司門口了,出發去餐廳,人數:" + staffs);
                    break;
                case 2:
                    System.out.println("大家都到餐廳了,開始用餐,人數:" + staffs);
                    break;

            }

            // 判斷是否只剩下主線程(一個參與者),如果是,則返回true,代表終止
            return registeredParties == 1;
        }
    };

    // 註冊主線程 ———— 讓主線程全程參與
    phaser.register();
    final StaffTask staffTask = new StaffTask();

    // 3個全程參與TB的員工
    for (int i = 0; i < 3; i++) {
        // 添加任務數
        phaser.register();
        new Thread(() -> {
            try {
                staffTask.step1Task();
                phaser.arriveAndAwaitAdvance();

                staffTask.step2Task();
                phaser.arriveAndAwaitAdvance();

                staffTask.step3Task();
                phaser.arriveAndAwaitAdvance();

                staffTask.step4Task();
                // 完成了,註銷離開
                phaser.arriveAndDeregister();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

    // 兩個不聚餐的員工加入
    for (int i = 0; i < 2; i++) {
        phaser.register();
        new Thread(() -> {
            try {
                staffTask.step1Task();
                phaser.arriveAndAwaitAdvance();

                staffTask.step2Task();
                System.out.println("員工【" + Thread.currentThread().getName() + "】回家了");
                // 完成了,註銷離開
                phaser.arriveAndDeregister();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

    while (!phaser.isTerminated()) {
        int phase = phaser.arriveAndAwaitAdvance();
        if (phase == 2) {
            // 到了去餐廳的階段,又新增4人,參加晚上的聚餐
            for (int i = 0; i < 4; i++) {
                phaser.register();
                new Thread(() -> {
                    try {
                        staffTask.step3Task();
                        phaser.arriveAndAwaitAdvance();

                        staffTask.step4Task();
                        // 完成了,註銷離開
                        phaser.arriveAndDeregister();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        }
    }
}

先給出運行結果,直觀感受下:

員工【Thread-0】從家出發了……
員工【Thread-2】從家出發了……
員工【Thread-1】從家出發了……
員工【Thread-3】從家出發了……
員工【Thread-4】從家出發了……
員工【Thread-4】到達公司
員工【Thread-0】到達公司
員工【Thread-1】到達公司
員工【Thread-3】到達公司
員工【Thread-2】到達公司
大家都到公司了,出發去公園,人數:5
員工【Thread-2】出發去公園玩
員工【Thread-1】出發去公園玩
員工【Thread-4】出發去公園玩
員工【Thread-3】出發去公園玩
員工【Thread-0】出發去公園玩
員工【Thread-1】到達公園門口集合
員工【Thread-2】到達公園門口集合
員工【Thread-0】到達公園門口集合
員工【Thread-3】到達公園門口集合
員工【Thread-3】回家了
員工【Thread-4】到達公園門口集合
員工【Thread-4】回家了
大家都到公司門口了,出發去餐廳,人數:3
員工【Thread-2】出發去餐廳
員工【Thread-0】出發去餐廳
員工【Thread-1】出發去餐廳
員工【Thread-5】出發去餐廳
員工【Thread-6】出發去餐廳
員工【Thread-7】出發去餐廳
員工【Thread-8】出發去餐廳
員工【Thread-8】到達餐廳
員工【Thread-7】到達餐廳
員工【Thread-1】到達餐廳
員工【Thread-5】到達餐廳
員工【Thread-2】到達餐廳
員工【Thread-6】到達餐廳
員工【Thread-0】到達餐廳
大家都到餐廳了,開始用餐,人數:7
員工【Thread-0】開始用餐
員工【Thread-8】開始用餐
員工【Thread-7】開始用餐
員工【Thread-1】開始用餐
員工【Thread-5】開始用餐
員工【Thread-2】開始用餐
員工【Thread-6】開始用餐
員工【Thread-5】用餐結束,回家
員工【Thread-2】用餐結束,回家
員工【Thread-7】用餐結束,回家
員工【Thread-1】用餐結束,回家
員工【Thread-6】用餐結束,回家
員工【Thread-8】用餐結束,回家
員工【Thread-0】用餐結束,回家

怎麼樣,各個階段有各的任務,並且各個階段參與者數量也不同。

代碼分析

1、Phaser 的創建

final Phaser phaser = new Phaser() {
    @Override
    protected boolean onAdvance(int phase, int registeredParties) {
        // 參與者數量,去除主線程
        int staffs = registeredParties - 1;
        switch (phase) {
            case 0:
                System.out.println("大家都到公司了,出發去公園,人數:" + staffs);
                break;
            case 1:
                System.out.println("大家都到公司門口了,出發去餐廳,人數:" + staffs);
                break;
            case 2:
                System.out.println("大家都到餐廳了,開始用餐,人數:" + staffs);
                break;

        }

        // 判斷是否只剩下主線程(一個參與者),如果是,則返回true,代表終止
        return registeredParties == 1;
    }
};

創建 Phaser 時,重寫了 onAdvance() 方法。這個方法類似於 CyclicBarrier多任務協同的利器 文中所講的CyclicBarrier的回調函數,在每個階段結束後,處理一些收尾工作。
不同的是,onAdvance() 方法更高級,方法入參直接告訴我們了當前階段,和該階段結束時的參與者數量;onAdvance() 方法簽名如下:

protected boolean onAdvance(int phase, int registeredParties) 

因此,重寫 onAdvance() 方法後,我們可以直接使用 phase 拿到當前階段,registeredParties 爲該階段結束時的參與者數量。

爲了不讓主進程結束,在創建完 phaser 對象後,立即註冊了參與者,該參與者是主線程,也就是讓主線程全程參與。

// 註冊主線程 ———— 讓主線程全程參與
phaser.register();

2、多階段任務協同
隨後,我們創建了3個線程,代表3個全程參與團建的員工;

for (int i = 0; i < 3; i++) {
    // 添加任務數
    phaser.register();
    new Thread(() -> {
        try {
            staffTask.step1Task();
            phaser.arriveAndAwaitAdvance();

            staffTask.step2Task();
            phaser.arriveAndAwaitAdvance();

            staffTask.step3Task();
            phaser.arriveAndAwaitAdvance();

            staffTask.step4Task();
            // 完成了,註銷離開
            phaser.arriveAndDeregister();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

在每次創建線程前,使用 phaser.register(); 添加參與者數量;

在參與者完成每個階段時,調用 phaser.arriveAndAwaitAdvance(); 進行協同等待,等所有參與者 都到達同步點後,再進入下一階段。
arriveAndAwaitAdvance() 從方法名也能看出,就是報告自己到達了同步點,並且協同、等待 onAdvance() 方法的執行。

在最後一個階段任務完成時,調用 phaser.arriveAndDeregister(); 代表:等這次協作完成後,我就離開。

接着,創建了2個線程,代表不聚餐的員工,線程的工作內容僅僅是前兩個階段的任務。

3、在第二階段,加入新的參與者
最後,用了一個 while 判斷,檢查 phaser 的任務階段,在第二階段,新增了四個參與者,繼續參加後續任務的協作。

while (!phaser.isTerminated()) {
    int phase = phaser.arriveAndAwaitAdvance();
    if (phase == 2) {
        // 到了去餐廳的階段,又新增4人,參加晚上的聚餐
        for (int i = 0; i < 4; i++) {
            phaser.register();
            new Thread(() -> {
                try {
                    staffTask.step3Task();
                    phaser.arriveAndAwaitAdvance();

                    staffTask.step4Task();
                    // 完成了,註銷離開
                    phaser.arriveAndDeregister();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

完整源碼
https://github.com/ljheee/JavaConcurrencyInPractice/blob/master/src/main/java/com/ljheee/juc/PhaserUsage.java

Phaser 核心方法

從上面示例的代碼,可以看到頻繁使用的的也就幾個方法:

  • arriveAndAwaitAdvance():類似於CyclicBarrier的await()方法,等待其它線程都到屏障點之後,再繼續執行。
  • arriveAndDeregister():把執行到此的線程從Phaser中註銷掉。
  • isTerminated():判斷 Phaser 是否終止。
  • register():將一個新的參與者註冊到 Phaser 中,這個新的參與者會被當成沒有執行本階段的線程
  • forceTermination():強制 Phaser 進入終止態。

多階段協同,示意圖如下:

Phaser 的父子層級

Phaser 支持層級,根root Phaser、父Phaser把每個子的 Phaser 當作父Phaser的一個parties,相當於把子 Phaser 內的一組參與者當初父Phaser的成員;這個子 Phaser 的內部有多少個parties線程,有多少階段,均可自定義。

父Phaser等待所有的parties都到達父的階段屏障,
即子Phaser的所有階段都執行完,也就是子Phaser都到達父的階段屏障,父Phaser纔會進入下一階段:喚醒所有的子Phaser的parties線程繼續執行下一階段。

CyclicBarrier 也可以作爲階段屏障使用,每個線程重複做爲CyclicBarrier的parties,但是沒辦法像Phaser那樣支持層級。
例如比賽,一個比賽分爲3個階段(phase): 初賽、複賽和決賽,規定所有運動員都完成上一個階段的比賽纔可以進行下一階段的比賽,並且比賽的過程中存在晉級、允許退賽(deregister),晉級成功且未退賽的才能進入下一階段,這個場景就很適合Phaser。

總結

JUC包下的CyclicBarrier、CountDownLatch、Phaser 三個都是線程同步輔助工具類,同步輔助三劍客。
CountDownLatch不能重用,CyclicBarrier、Phaser都可以重用,並且Phaser
更加靈活可以在運行期間隨時加入(register)新的parties,也可以在運行期間隨時退出(deregister)。

關於 CyclicBarrier、CountDownLatch 可閱讀 CountDownLatch的兩種常用場景CyclicBarrier多任務協同的利器

閱讀原文:https://mp.weixin.qq.com/s/e_3SDMW5pAG48oXQBaS1Zg


推薦閱讀

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

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