java多執行緒系列:通過對戰遊戲學習CyclicBarrier

雲梟發表於2018-08-31

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物件。而且每一次到達都會執行一次。

CyclicBarrier和CountDownLatch的區別

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方法都是呼叫CyclicBarrierdowait(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異常

案例原始碼地址:github.com/rainbowda/l…

覺得不錯的點個Star,謝謝

相關文章