面試官:說一說CyclicBarrier的妙用!我:這個沒用過

JavaBuild發表於2024-04-16

寫在開頭

面試官:同學,AQS的原理知道嗎?
我:學過一點,抽象佇列同步器,Java中很多同步工具都是基於它的...
面試官:好的,那其中CyclicBarrier學過嗎?講一講它的妙用吧
我:啊,這個,這個我平時寫程式碼沒用過...
面試官:那你回去再學學吧!

隨著Java的國內競爭環境逐漸激烈,面試時遇到很多奇葩的問題也是越來越多,以上是模擬的一個面試場景,同學們看下你們能答得上來不?😝

什麼是CyclicBarrier?

在過去的幾天裡,我們基於AQS學習了不少內容,其中基於AQS構建的同步工具類也學了Semaphore(訊號量)和CountDownLatch(倒數計時器),甚至於也手撕過同步器,今天我們繼續來學習另外一個同步類:CyclicBarrier

CyclicBarrier(迴圈屏障):讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續幹活。

CyclicBarrier的原理

在CyclicBarrier有兩個成員變數分別為partiescount,前者代表每次攔截的執行緒數量,後者是初始化時保持和parties相等的計數標識,每有一個執行緒執行到同步點時,count減1,當count值變為0時說明所有執行緒都走到了同步點,這時就可以嘗試執行我們在構造方法中設計的任務啦。

【原始碼解析1】

//每次攔截的執行緒數
private final int parties;
//計數器
private int count;

//一個引數的構造
public CyclicBarrier(int parties) {
    this(parties, null);
}
//多參構造,parties為攔截執行緒數,barrierAction這個 Runnable會在 CyclicBarrier 的計數器為 0 的時候執行,用來完成更復雜的任務。
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

每個執行緒透過呼叫await方法告訴CyclicBarrier已經到達屏障,然後進行阻塞等待,知道count等於0,所有執行緒都到達了屏障,因此,我們跟入await方法的原始碼中去看一下。

【原始碼解析2】

public int await() throws InterruptedException, BrokenBarrierException {
  try {
      return dowait(false, 0L);
  } catch (TimeoutException toe) {
      throw new Error(toe); // cannot happen
  }
}
//await方法內部,繼續呼叫dowait方法實現功能
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()) {
       		//打破屏障
            breakBarrier();
            throw new InterruptedException();
        }
        // cout減1
        int index = --count;
        // 當 count 數量減為 0 之後說明最後一個執行緒已經到達柵欄了,也就是達到了可以執行await 方法之後的條件
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                // 將 count 重置為 parties 屬性的初始化值
                // 喚醒之前等待的執行緒
                // 下一波執行開始
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        for (;;) {
            try {
                if (!timed)
                    trip.await();
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    // We're about to finish waiting even if we had not
                    // been interrupted, so this interrupt is deemed to
                    // "belong" to subsequent execution.
                    Thread.currentThread().interrupt();
                }
            }

            if (g.broken)
                throw new BrokenBarrierException();

            if (g != generation)
                return index;

            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

在dowait(boolean timed, long nanos),可以透過時間引數來設定阻塞的時間,預設為false,在這個方法內部,每次執行緒呼叫await後,都會進行--count操作,直到index為0時,會去執行command,然後喚醒執行緒繼續向下執行,CyclicBarrier 的計數器可以透過reset()方法重置,所以它能處理迴圈使用的場景。

CyclicBarrier的使用

大致的瞭解了CyclicBarrier的原理之後,我們寫個小demo測試一下它如何使用

【程式碼示例】

public class Test {
    public static void main(String[] args) {
        int numberOfThreads = 3; // 執行緒數量
        CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> {
            // 當所有執行緒都到達障礙點時執行的操作
            System.out.println("所有執行緒都已到達屏障,進入下一階段");
        });

        for (int i = 0; i < numberOfThreads; i++) {
            new Thread(new Task(barrier), "Thread " + (i + 1)).start();
        }
    }

    static class Task implements Runnable {
        private final CyclicBarrier barrier;

        public Task(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " 正在屏障處等待");
                barrier.await(); // 等待所有執行緒到達障礙點
                System.out.println(Thread.currentThread().getName() + " 已越過屏障.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

輸出:

Thread 2 正在屏障處等待
Thread 1 正在屏障處等待
Thread 3 正在屏障處等待
所有執行緒都已到達屏障,進入下一階段
Thread 3 已越過屏障.
Thread 1 已越過屏障.
Thread 2 已越過屏障.

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!
image

相關文章