併發工具類CyclicBarrier的源碼分析以及使用場景

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,即可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程文章。

微信公衆號

上一篇文章介紹了工具類CountDownLatch的原理和使用場景(併發工具類CountDownLatch的源碼分析以及使用場景),今天將介紹JUC包下另一個十分常用的併發工具類CyclicBarrier,翻譯過來就是可循環使用的屏障

簡介

  • CyclicBarrier的功能與CountDownLatch的功能十分類似,也是控制線程的執行順序,但是它與CountDownLatch的區別是,CyclicBarrier是讓一組線程阻塞在同一屏障(同步點)處,直到最後一個線程到達屏障(也就是屏障的計數器減爲0),屏障纔會打開,這些阻塞在屏障處的線程纔會繼續往下執行。CountDownLatch是讓一組或者一個線程等待其他線程執行完後,當前線程才繼續執行。另外一點區別就是,CyclicBarrier的計數器減爲0後,可以重置計數器,從而可以再次使用,這一點通過類名中含有Cyclic(循環)就能看出。而CountDownLatch的計數器減爲0後,不會重置,因此不能重複使用。

示例

  • CyclicBarrier的使用也十分簡單,只需要new一個CyclicBarrier創建一個實例對象,它的構造方法中需要傳入一個int類型的參數,用來指定屏障的大小(即當多少個線程到達屏障後,屏障打開),然後在線程中調用await()方法即可讓線程阻塞在屏障處。
  • 如下Demo示例中,通過10個線程模擬了十個短跑運動員。在現實生活中,100米賽跑的時候,運動員需要聽到發令槍響之後才能起跑,所有運動員的起跑時間是在同一個時間的,不能搶跑。發令槍就相當於程序中的CyclicBarrier,當所有人準備好,聽到發令槍響(達到屏障)時,才能開始起跑。
public class CyclicBarrierDemo {

    public static void main(String[] args) {
        Random random = new Random();
        CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
        List<Thread> threads = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(()->{
                int time = random.nextInt(5) + 1;
                try {
                    // 通過線程休眠來模擬每位運動員的準備時間
                    Thread.sleep(time * 1000);
                    System.out.println(Thread.currentThread().getName() + "準備就緒");
                    // 運動員準備就緒後,就示意發令員自己準備好了,即調用await()方法
                    cyclicBarrier.await();
                    System.out.println("起跑槍響,"+Thread.currentThread().getName() + "起跑");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }

            },"運動員"+(i+1)));
        }
        
        for (Thread thread : threads) {
            thread.start();
        }

    }
}

實現原理

  • CyclicBarrierCoutDownLatch的底層實現也存在一點區別,CountDownLatch底層是直接通過組合一個繼承了AQS的同步組件來實現的,而CyclicBarrier並沒有直接藉助AQS的同步組件,而是通過組合ReentrantLock這把鎖來實現的(ReentrantLock的底層實現依然使用的AQS來實現的,歸根結底,CyclicBarrier的底層實現也是AQS)。
  • 由於CyclicBarrier是使用ReentrantLock來實現的,因此它有個屬性是lock。在CyclicBarrier中還維護了一個計數器:count。由於CyclicBarrier可以重複使用,即計數器減爲0後,將其重置,因此還需要藉助另外一個變量來存放count的初始值,這個變量就是parties。CyclicBarrier中有個屬性是generation,其類型是一個CyclicBarrier的內部類Generation,它的作用是用來實現await(long timeout,TimeUnit unit)方法的超時等待的功能(後面分析源碼時會詳細解釋)。當CyclicBarrier重置時,也會重新令generation重置賦值。CyclicBarrier的屬性和方法見下表。
屬性或者方法 作用
ReentrantLock lock 用來保證線程安全,防止多個線程同時修改count時,出現線程不安全的情況
int count 計數器,當調用await()方法時,會令count減1
int parties 記錄計數器的初始值
Generation generation 當計數器重置時,也會重置該屬性。當出現超時等待時,會令generation中的broken屬性爲true。
Condition trip 等待隊列
Runnable barrierCommand CyclicBarrier支持當計數器減爲0後,先執行一個Runnable任務,然後執行阻塞在屏障處的線程
await() 讓線程等待在阻塞在屏障處,並令計數器減1,不支持超時等待
await(long timeout, TimeUnit unit) 讓線程等待在阻塞在屏障處,最大等待timeout的單位時間,並令計數器減1
reset() 重置屏障
  • CyclicBarrier有兩個有參構造器,如下。
// parties用來指定計數器的大小
// barrierAction是一個Runnable,當計數器減爲0時,會先執行barrierAction,然後再打開屏障
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

// parties用來指定計數器的大小
public CyclicBarrier(int parties) {
    // 調用有兩個參數的有參構造方法
    this(parties, null);
}
  • 當執行CyclicBarrier cyclicBarrier = new CyclicBarrier(10);這一行代碼時,會初始化計數器count的值和parties。傳入的參數10,表示當有10個線程到達屏障時,纔會打開屏障。
  • 當調用cyclicBarrier.await()時,在await()方法中會直接調用dowait()方法。dowait()方法的源碼如下。
private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        final Generation g = generation;
        // 如果有線程調用了await(long timeout,TimeUnit unit)方法,且出現了超時等待,那麼此時g.broken就爲true,因此會拋出異常
        if (g.broken)
            throw new BrokenBarrierException();

        // 如果線程被中斷,那麼就直接中斷屏障(讓所有等待的線程醒來)
        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }
        // 計數器遞減
        int index = --count;
        // 如果遞減後的結果爲0,說明所有線程達到屏障
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                // 判斷有沒有需要優先執行的任務,有就執行
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                // 在nextGeneration()會喚醒等待隊列中的所有線程,邊讓計數器的count值重置
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        // 如果計數器沒有減到0,就讓當前線程進入到等待隊列中等待
        for (;;) {
            try {
                // timed是用來標識是否是超時等待
                if (!timed)
                    // 調用condition的await()方法,進入到等待隊列
                    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()方法中會先判斷generation.broken是否爲true,在第一次進入時,肯定爲false(默認值就是false),該字段的值只有當線程調用await(long timeout,TimeUint unit)方法,且出現了超時情況,纔會爲true。
  • 然後將計數器count減1,如果減一之後count爲0,表示此時所有線程都已經到達了屏障,此時就可以打開屏障,讓阻塞的線程繼續執行了。但是在打開屏障之前,會先因判斷barrierCommand是否爲空,如果不爲空,就先執行barrierCommand。然後才調用nextGeneration()方法。nextGeneration()的主要作用是喚醒等待隊列中的所有線程,並重置計數器。其源碼如下。
private void nextGeneration() {
    // signal completion of last generation
    // 喚醒等待隊列中所有在等待的線程
    trip.signalAll();
    // set up next generation
    count = parties;
    generation = new Generation();
}
  • 如果count減一之後的值不爲0,就表示還有線程沒有到達屏障,還不能打開屏障,因此就需要令當前線程加入到等待隊列中,即會調用trip.await(),讓線程等待。
  • 如果出現等待超時了,就會執行到for循環中的catch語句塊中,在catch語句塊中調用了breakBarrier()方法,breakBarrier()方法的主要作用就是將generation的broken屬性設置true。那麼當執到if(g.broken)就會判斷成立,然後拋出異常,這樣就實現了超時等待功能。
private void breakBarrier() {
    generation.broken = true;
    count = parties;
    trip.signalAll();
}
  • 如果沒有出現超時等待,當計數器減爲0時,就會喚醒trip等待隊列中的所有線程,使其進入到Lock的同步隊列中,接下來就是在Lock的同步隊列中,一個節點一個節點的線程被喚醒,然後當線程從trip.await()方法處醒來,繼續執行後面的邏輯。關於ReentrantLock的詳細分析可以參考這兩篇文章:可重入鎖(ReentrantLock)源碼分析 公平鎖與非公平鎖的對比
  • 至於Cyclicbarrier的await(long timeout,TimeUnit unit)方法的實現,最終也是調用dowait()方法,因此這裏就不再詳細說明。整體來說,CyclicBarrier的源碼實現相對比較簡單。

與CountDownLatch的區別

  • CyclicBarrier與CountDownLacth存在幾點區別。首先CyclicBarrier是讓所有線程到達屏障後再一起執行後面的邏輯,而CountDownLatch是讓一個線程或者一組線程等待其他線程執行完後,自己再接着執行。第二,CyclicBarrier的計數器可以重置,因此可以重複使用,而CountDownLatch的計數器不能重置,不可以重複使用。第三,CyclicBarrier可以在所有線程達到屏障後,先執行一個Runnable任務,然後纔打開屏障,這個功能在特殊場景下很有用處。第四,雖然兩者最終底層實現都是根據AQS來實現的,但是CyclicBarrier是通過ReentrantLock這個互斥鎖來間接使用AQS實現的,而CountDownLatch是直接使用AQS的共享鎖來實現的。
  • 關於CyclicBarrier的構造方法中支持傳入一個Runnable類型的參數,下面還是以文章開頭的Demo,演示一下其用法。在上面的Demo中,運動員都準備好站到起跑線後,此時應該是發令員先鳴槍,然後運動員纔開始起跑,也就是線程開始執行,那麼這個鳴槍的動作就是在屏障打開之前,那麼我們一個通過Runnable來實現。示例代碼如下。
public class CyclicBarrierDemo {

    public static void main(String[] args) {
        Random random = new Random();
        // 在CyclicBarrier構造方法中,第二個參數傳入一個Runnable。
        CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Runnable() {
            @Override
            public void run() {
                System.out.println("==============  各就位!!!預備!!!砰!============");
            }
        });
        List<Thread> threads = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(()->{
                int time = random.nextInt(5) + 1;
                try {
                    Thread.sleep(time * 1000);
                    System.out.println(Thread.currentThread().getName() + "準備就緒");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName() + "起跑");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }

            },"運動員"+(i+1)));
        }

        for (Thread thread : threads) {
            thread.start();
        }

    }
}
  • 從打印結果中可以看到,當所有運動員就緒後,會先打印出============== 各就位!!!預備!!!砰!============這一行後,纔會讓其他線程繼續執行。

總結

  • 本文詳細介紹了CyclicBarrier的功能,以及如何使用,然後結合源碼分析了CyclicBarrier的實現原理,並從功能上和底層實現原理上,對比了CyclicBarrier和CountDownLatch的區別,最後總結一下,在大部分場景下,CountDownLatch能實現的功能,都能使用CyclicBarrier實現。

推薦

微信公衆號

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