1. 程式人生 > >阿里一面CyclicBarrier和CountDownLatch的區別是啥

阿里一面CyclicBarrier和CountDownLatch的區別是啥

# 引言 前面一篇文章我們[《Java高併發程式設計基礎三大利器之CountDownLatch》](https://mp.weixin.qq.com/s/ga3x8LYxDMgCzdfn6UUT_w)它有一個缺點,就是它的計數器只能夠使用一次,也就是說當計數器(`state`)減到為 `0`的時候,如果 再有執行緒呼叫去 `await`() 方法,該執行緒會直接通過,不會再起到等待其他執行緒執行結果起到同步的作用。為了解決這個問題`CyclicBarrier`就應運而生了。 # 什麼是CyclicBarrier `CyclicBarrier`是什麼?把它拆開來翻譯就是迴圈(`Cycle`)和屏障(`Barrier`) ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210317002051546.png) 它的主要作用其實和`CountDownLanch`差不多,都是讓一組執行緒到達一個屏障時被阻塞,直到最後一個執行緒到達屏障時,屏障會被開啟,所有被屏障阻塞的執行緒才會繼續執行,不過它是可以迴圈執行的,這是它與`CountDownLanch`最大的不同。`CountDownLanch`是隻有當最後一個執行緒把計數器置為`0`的時候,其他阻塞的執行緒才會繼續執行。學習`CyclicBarrier`之前建議先去看看這幾篇文章: - [《Java高併發程式設計基礎之AQS》](https://mp.weixin.qq.com/s/Scz_puodkdtoA6Zz7nh4uA) - [《Java高併發程式設計基礎三大利器之Semaphore》](https://mp.weixin.qq.com/s/ic1lX1G3kYvmztTgN0Yihg) - [《Java高併發程式設計基礎三大利器之CountDownLatch》](https://mp.weixin.qq.com/s/ga3x8LYxDMgCzdfn6UUT_w) # 如何使用 我們首先先來看下關於使用`CyclicBarrier`的一個`demo`:比如遊戲中有個關卡的時候,每次進入下一關的時候都需要進行載入一些地圖、特效背景音樂什麼的只有全部載入完了才能夠進行遊戲: ```java /**demo 來源https://blog.csdn.net/lstcui/article/details/107389371 * 公眾號【java金融】 */ public class CyclicBarrierExample { static class PreTaskThread implements Runnable { private String task; private CyclicBarrier cyclicBarrier; public PreTaskThread(String task, CyclicBarrier cyclicBarrier) { this.task = task; this.cyclicBarrier = cyclicBarrier; } @Override public void run() { for (int i = 0; i < 4; i++) { Random random = new Random(); try { Thread.sleep(random.nextInt(1000)); System.out.println(String.format("關卡 %d 的任務 %s 完成", i, task)); cyclicBarrier.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } } } public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> { System.out.println("本關卡所有的前置任務完成,開始遊戲... ..."); }); new Thread(new PreTaskThread("載入地圖資料", cyclicBarrier)).start(); new Thread(new PreTaskThread("載入人物模型", cyclicBarrier)).start(); new Thread(new PreTaskThread("載入背景音樂", cyclicBarrier)).start(); } } } ``` 輸出結果如下: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210317155138530.png) 我們可以看到每次遊戲開始都會等當前關卡把遊戲的人物模型,地圖資料、背景音樂載入完成後才會開始進行遊戲。並且還是可以迴圈控制的。 # 原始碼分析 ### 結構組成 ```java /** The lock for guarding barrier entry */ private final ReentrantLock lock = new ReentrantLock(); /** Condition to wait on until tripped */ private final Condition trip = lock.newCondition(); /** The number of parties */ private final int parties; /* The command to run when tripped */ private final Runnable barrierCommand; /** The current generation */ private Generation generation = new Generation(); ``` - **lock**:用於保護屏障入口的鎖 - **trip** :達到屏障並且不能放行的執行緒在trip條件變數上等待 - **parties** :柵欄開啟需要的到達執行緒總數 - **barrierCommand**:最後一個執行緒到達屏障後執行的回撥任務 - **generation**:這是一個內部類,通過它實現`CyclicBarrier`重複利用,每當`await`達到最大次數的時候,就會重新`new` 一個,表示進入了下一個輪迴。裡面只有一個`boolean`型屬性,用來表示當前輪迴是否有執行緒中斷。 ### 主要方法 `await`方法 ```java public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen } } /** * Main barrier code, covering the various policies. */ private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { final ReentrantLock lock = this.lock; lock.lock(); try { //獲取barrier當前的 “代”也就是當前迴圈 final Generation g = generation; if (g.broken) throw new BrokenBarrierException(); if (Thread.interrupted()) { breakBarrier(); throw new InterruptedException(); } // 每來一個執行緒呼叫await方法都會進行減1 int index = --count; if (index == 0) { // tripped boolean ranAction = false; try { final Runnable command = barrierCommand; // new CyclicBarrier 傳入 的barrierCommand, command.run()這個方法是同步的,如果耗時比較多的話,是否執行的時候需要考慮下是否非同步來執行。 if (command != null) command.run(); ranAction = true; // 這個方法1. 喚醒所有阻塞的執行緒,2. 重置下count(count 每來一個執行緒都會進行減1)和generation,以便於下次迴圈。 nextGeneration(); return 0; } finally { if (!ranAction) breakBarrier(); } } // loop until tripped, broken, interrupted, or timed out for (;;) { try { // 進入if條件,說明是不帶超時的await if (!timed) // 當前執行緒會釋放掉lock,然後進入到trip條件佇列的尾部,然後掛起自己,等待被喚醒。 trip.await(); else if (nanos > 0L) //說明當前執行緒呼叫await方法時 是指定了 超時時間的! nanos = trip.awaitNanos(nanos); } catch (InterruptedException ie) { //Node節點在 條件佇列內 時 收到中斷訊號時 會丟擲中斷異常! //g == generation 成立,說明當前代並沒有變化。 //! g.broken 當前代如果沒有被打破,那麼當前執行緒就去打破,並且丟擲異常.. 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. //執行到else有幾種情況? //1.代發生了變化,這個時候就不需要丟擲中斷異常了,因為 代已經更新了,這裡喚醒後就走正常邏輯了..只不過設定下 中斷標記。 //2.代沒有發生變化,但是代被打破了,此時也不用返回中斷異常,執行到下面的時候會丟擲 brokenBarrier異常。也記錄下中斷標記位。 Thread.currentThread().interrupt(); } } //喚醒後,執行到這裡,有幾種情況? //1.正常情況,當前barrier開啟了新的一代(trip.signalAll()) //2.當前Generation被打破,此時也會喚醒所有在trip上掛起的執行緒 //3.當前執行緒trip中等待超時,然後主動轉移到 阻塞佇列 然後獲取到鎖 喚醒。 if (g.broken) throw new BrokenBarrierException(); //喚醒後,執行到這裡,有幾種情況? //1.正常情況,當前barrier開啟了新的一代(trip.signalAll()) //2.當前執行緒trip中等待超時,然後主動轉移到 阻塞佇列 然後獲取到鎖 喚醒。 if (g != generation) return index; //喚醒後,執行到這裡,有幾種情況? //.當前執行緒trip中等待超時,然後主動轉移到 阻塞佇列 然後獲取到鎖 喚醒。 if (timed && nanos <= 0L) { breakBarrier(); throw new TimeoutException(); } } } finally { lock.unlock(); } } ``` ### 小結 **到了這裡我們是不是可以知道為啥`CyclicBarrier`可以進行迴圈計數?** `CyclicBarrier`採用一個內部類`Generation`來維護當前迴圈,每一個`await`方法都會儲存當前的`generation`,獲取到相同`generation`物件的屬於同一組,每當`count`的次數耗盡就會重新`new`一個`Generation`並且重新設定`count`的值為`parties`,表示進入下一次新的迴圈。 從這個`await`方法我們是不是可以知道只要有一個執行緒被中斷了,當代的 `generation`的`broken` 就會被設定為`true`,所以會導致其他的執行緒也會被丟擲`BrokenBarrierException`。相當於一個失敗其他也必須失敗,感覺有“強一致性“的味道。 # 總結 - `CountDownLanch`是為計數器是設定一個值,當多次執行`countdown`後,計數器減為`0`的時候所有執行緒被喚醒,然後`CountDownLanch`失效,只能夠使用一次。 - `CyclicBarrier`是當`count`為`0`時同樣喚醒全部執行緒,同時會重新設定`count`為`parties`,重新`new`一個`generation`來實現重複利用。 ### 結束 - 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。 - 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。 - 感謝您的閱讀,十分歡迎並感謝您的關注。 巨人的肩膀摘蘋果 https://javajr.cn/ http://www.360doc.com/content/20/0812/08/55930996_929792021.shtml https://www.cnblogs.com/xxyyy/p/12958