天天看點

java多線程系列:通過對戰遊戲學習CyclicBarrier

java多線程系列:通過對戰遊戲學習CyclicBarrier

CyclicBarrier是java.util.concurrent包下面的一個工具類,字面意思是可循環使用(Cyclic)的屏障(Barrier),通過它可以實作讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,所有被屏障攔截的線程才會繼續執行。

這篇文章将介紹CyclicBarrier這個同步工具類的以下幾點

  1. 通過案例分析
  2. 兩種不同構造函數測試
  3. CyclicBarrier和CountDownLatch的差別
  4. await方法及源碼分析。

需求

繼上一篇

CountDownLatch

模拟遊戲加載後,現在使用者點選開始按鈕後,需要比對包括自己在内的五個玩家才能開始遊戲,比對玩家成功後進入到選擇角色階段。當5位玩家角色都選擇完畢後,開始進入遊戲。進入遊戲時需要加載相關的資料,待全部玩家都加載完畢後正式開始遊戲。

解決方案

從需求中可以知道,想要開始遊戲需要經過三個階段,分别是

  1. 比對玩家
  2. 選擇角色
  3. 加載資料

在這三個階段中,都需要互相等待對方完成才能繼續進入下個階段。

這時可以采用

CyclicBarrier

來作為各個階段的節點,等待其他玩家到達,在進入下個階段。

java多線程系列:通過對戰遊戲學習CyclicBarrier

定義繼承Runnable的類

這裡名稱就叫做

StartGame

,包含兩個屬性

private String player;
private CyclicBarrier barrier;           

通過構造函數初始化兩個屬性

public StartGame(String player, CyclicBarrier barrier) {
    this.player = player;
    this.barrier = barrier;
}           

run方法如下

public void run() {
    try {
        System.out.println(this.getPlayer()+" 開始比對玩家...");
        findOtherPlayer();
        barrier.await();

        System.out.println(this.getPlayer()+" 進行選擇角色...");
        choiceRole();
        System.out.println(this.getPlayer()+" 角色選擇完畢等待其他玩家...");
        barrier.await();

        System.out.println(this.getPlayer()+" 開始遊戲,進行遊戲加載...");
        loading();
        System.out.println(this.getPlayer()+" 遊戲加載完畢等待其他玩家加載完成...");
        barrier.await();


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

其他的方法findOtherPlayer()、choiceRole()等待使用

Thread.sleep()           

來模拟花費時間

編寫測試代碼

CyclicBarrier有兩個構造函數,如下

public CyclicBarrier(int parties) {}
public CyclicBarrier(int parties, Runnable barrierAction) {}           

先來看看一個參數的構造函數

CyclicBarrier(int parties)

public static void main(String[] args) throws IOException {
    CyclicBarrier barrier = new CyclicBarrier(5);

    Thread player1 = new Thread(new StartGame("1",barrier));
    Thread player2 = new Thread(new StartGame("2",barrier));
    Thread player3 = new Thread(new StartGame("3",barrier));
    Thread player4 = new Thread(new StartGame("4",barrier));
    Thread player5 = new Thread(new StartGame("5",barrier));

    player1.start();
    player2.start();
    player3.start();
    player4.start();
    player5.start();

    System.in.read();
}           

測試結果如下

java多線程系列:通過對戰遊戲學習CyclicBarrier

CyclicBarrier(int parties, Runnable barrierAction)

CyclicBarrier barrier = new CyclicBarrier(5);           

替換為

CyclicBarrier barrier = new CyclicBarrier(5, () -> {
    try {
        System.out.println("階段完成,等待2秒...");
        Thread.sleep(2000);
        System.out.println("進入下個階段...");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

});           

再來看看效果

java多線程系列:通過對戰遊戲學習CyclicBarrier

可以看到在到達某個節點時,會執行執行個體化CyclicBarrier時傳入的Runnable對象。而且每一次到達都會執行一次。

CountDownLatch CyclicBarrier
計數為0時,無法重置 計數達到0時,計數置為傳入的值重新開始
調用countDown()方法計數減一,調用await()方法隻進行阻塞,對計數沒任何影響 調用await()方法計數減一,若減一後的值不等于0,則線程阻塞
不可重複使用 可重複使用

await方法

public int await(){}
public int await(long timeout, TimeUnit unit){}           

無參的await方法這裡就不做介紹了,主要介紹下有參的await方法。

有參的await方法傳入兩個參數,一個是時間、另一個是時間機關

當調用有參的await方法時會出現下方兩個異常

java.util.concurrent.TimeoutException
java.util.concurrent.BrokenBarrierException           

TimeoutException異常是指調用

await

方法後等待時間超過傳入的時間,此時會将

CyclicBarrier

的狀态變成broken,其他調用

await

方法将會抛出BrokenBarrierException異常,這時的

CyclicBarrier

将變得不可用,需要調用

reset()

方法重置

CyclicBarrier

的狀态。

為什麼這麼說?

源碼分析一波就可以看出來了

不管是有參還是無參的await方法都是調用

CyclicBarrier

dowait(boolean timed, long nanos)

方法,這個方法代碼太長了,截取部分貼出來

private int dowait(boolean timed, long nanos){
    //加鎖、try catch代碼
    final Generation g = generation;
    //判斷栅欄的狀态
    if (g.broken)
        throw new BrokenBarrierException();
    //...省略

    int index = --count;
    //(index == 0) 時的代碼,省略

    for (;;) {
        try {
            if (!timed)
                trip.await();
            else if (nanos > 0L)
                nanos = trip.awaitNanos(nanos);
        } catch (InterruptedException ie) {}

        //判斷栅欄的狀态
        if (g.broken)
            throw new BrokenBarrierException();

        if (g != generation)
            return index;
        //判斷是否是定時的,且已經逾時了
        if (timed && nanos <= 0L) {
            //打破栅欄的狀态
            breakBarrier();
            throw new TimeoutException();
        }
    }
    //解鎖
}           

在代碼的尾部進行判斷目前等待是否已經逾時,如果是會調用

breakBarrier()

方法,且抛出TimeoutException異常,下面是

breakBarrier()

的代碼

private void breakBarrier() {
    generation.broken = true;
    count = parties;
    trip.signalAll();
}           

代碼中将broken狀态置為true,表示目前栅欄移除損壞狀态,且重置栅欄數量,然後喚醒其他等待的線程。此時被喚醒的線程或者其他線程進入dowait方法時,都會抛出BrokenBarrierException異常

案例源代碼位址:

https://github.com/rainbowda/learnWay/tree/master/learnConcurrency/src/main/java/com/learnConcurrency/utils/cyclicBarrier

覺得不錯的點個Star,謝謝