【多線程】CyclicBarrier實現原理

前言

CyclicBarrier,字面意思“循環屏障”,用於多個線程一起到達屏障點後,多個線程再一起接着運行的情況。例如,線程1和線程2一起運行,線程1運行到屏障點a時,將會被阻塞,等到線程2運行到屏障點a後,線程1和線程2纔可以打破屏障,接着運行。如果有屏障點b,則他們需要像打破屏障a一樣打破屏障b,如此循環往復。


常用方法

public class CyclicBarrier {
 
    //構造方法,傳入線程總數以及打破屏障點前的任務
    public CyclicBarrier(int parties, Runnable barrierAction);

    //構造方法,傳入線程總數,不需要執行額外任務
    public CyclicBarrier(int parties);
     
    //在所有一起到達屏障點前,阻塞當前線程
    public int await();
}

下面給出一個簡單的示例:

package com.yang.testCB;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class Main {
    public static void main(String[] args) {
        CyclicBarrier cb = new CyclicBarrier(2, () -> {
            System.out.println("即將打破屏障");
        });

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "開始運行");
                try {
                    cb.await();
                    System.out.println(Thread.currentThread().getName() + "已經穿越了第1個屏障");
                    cb.await();
                    System.out.println(Thread.currentThread().getName() + "已經穿越了第2個屏障");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

輸出如下:

由上面的例子,我們發現,線程0與線程1調用await()後,在所有線程到達第1個屏障點前,都會被阻塞。最後一個線程到達屏障點後,先執行額外任務,才能打破屏障,接着該線程喚醒其他阻塞的線程,此時所有線程繼續運行。


原理解析

先看一下CyclicBarrier裏面有哪些變量

public class CyclicBarrier {
  
    //同步鎖
    private final ReentrantLock lock = new ReentrantLock();

    //條件隊列
    private final Condition trip = lock.newCondition();

    //傳入構造方法的線程總數
    private final int parties;

    //打破屏障前需要執行的任務,或者稱爲換代任務
    private final Runnable barrierCommand;

    //當前代
    private Generation generation = new Generation();
    
    //計數器,代表當前還未達到屏障的線程數目。
    private int count;

}
  • CyclicBarrier藉助ReentranLock與Condition來對線程進行阻塞的。
  • parties是傳入構造方法的線程總數,在該CyclicBarrier實例的整個生命週期內,該值保持不變,並且會在換代的時候,使得count=parties
  • barrierCommand,換代任務,打破屏障前需要執行的任務,任務執行完成後(不管成功還是失敗),纔會喚醒所有線程
  • generation代表當前代,兩個屏障之間稱爲一代,原點與第一個屏障可以稱爲第一代
  • count,計數器,每有一個線程到達屏障時,count值就會減1。一旦減爲0後,則先同步執行換代任務,接着打破屏障,開啓下一代,然後喚醒所有阻塞的線程,最後將count重置爲parties。

CyclicBarrier內的主要方法:

構造方法

 public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }

    public CyclicBarrier(int parties) {
        this(parties, null);
    }

CyclicBarrier提供兩個構造方法,不過最核心的是兩參數的構造方法。構造方法中設置了parties與count的值都是傳入的線程總數,barrierAction爲換代任務,當然也可以不指定換代任務。

await()方法

 public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

    public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

CyclicBarrier同樣提供了定時等待與非定時等待,不過都調用了dowait()方法,該方法是CyclicBarrier內最爲核心的方法。

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //當前代
            final Generation g = generation;
            //判斷當前代的狀態,如果當前代後的屏障被打破,則g.broken返回true,否則返回false。
            if (g.broken)
                throw new BrokenBarrierException();
            //判斷當前線程是否被中斷
            if (Thread.interrupted()) {
                //如果當前線程已經被中斷,則調用breakBarrier()
                //該方法代碼爲generation.broken = true;count = parties;trip.signalAll();
                //可見,只做了3件事:先將當前代的屏障變爲打破狀態,接着重置計數器的值,最後喚醒所有被阻塞的線程
                breakBarrier();
                //最後拋出中斷異常
                throw new InterruptedException();
            }
            //將計數器的值減1
            int index = --count;
            if (index == 0) {
                //如果當前計數器的值爲0
                boolean ranAction = false;
                try {
                    //則先執行換代任務,可以看得出來,是由最後一個到達屏障的線程執行的
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //開啓下一代,這個方法的代碼爲trip.signalAll();count = parties;generation = new Generation();
                    //該代碼喚醒所有被阻塞的線程,重置計數器的值,並且實例化下一代
                    nextGeneration();
                    return 0;
                } finally {
                    //如果換代任務未執行成功,則先將當前代的屏障變爲打破狀態,接着重置計數器的值,最後喚醒所有被阻塞的線程
                    if (!ranAction)
                        breakBarrier();
                }
            }

            //當前線程一直阻塞,直到“有parties個線程到達barrier” 或 “當前線程被中斷” 或 “超時”這3者之一發生
            //死循環
            for (;;) {
                try {
                    if (!timed)
                        //如果不是定時等待,則調用條件隊列的await()進行阻塞
                        trip.await();
                    else if (nanos > 0L)
                        //如果是定時等待,則調用條件隊列的awaitNanos進行等待
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    //如果在等待過程中,當前線程被打斷
                    if (g == generation && ! g.broken) {
                        //被打斷後,還處於當前代,且當前代的屏障也未被打破
                        //現在的情況是,最後一個線程還未到屏障,當前線程早早到了,並且在進行等待,但是在等待的過程中,被打斷了。
                        //則打破當前代的屏障,喚醒所有被阻塞的線程
                        breakBarrier();
                        throw ie;
                    } else {
                        //如果已經換代,則手動進行打斷
                        Thread.currentThread().interrupt();
                    }
                }
                //此時線程被喚醒,需要判斷自己爲什麼被喚醒了                
                
                //如果是其他某個線程被打斷或者是由於超時導致當前代的屏障被打破,則拋出異常
                if (g.broken)
                    throw new BrokenBarrierException();

                //如果是正常換代,則返回index值
                if (g != generation)
                    return index;

                //如果是定時等待,且時間已經到了,則打破屏障,喚醒所有阻塞的線程,最後拋出異常
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

用一開始的例子,簡單說下整個流程:

線程0首先運行,輸出“Thread-0開始運行”,接着調用了await()方法,然後進入dowait()方法中,獲得lock鎖,此時count-1=1,count值不爲0,因此進入了for循環中,最後調用了trip.await()方法,於是線程0釋放了lock鎖,被阻塞住了。

線程1和線程0差不多同時運行,但線程0首先獲取到了鎖,線程1輸出“Thread1-開始運行”後,需要等待線程0釋放鎖。此時線程0釋放了lock鎖,線程1可以進入到同步代碼中,此時count-1=0,因此線程1首先執行換代任務,輸出“即將打破屏障”。接着調用nextGeneration()方法開啓下一代,最後直接返回0,然後輸出“Thread1-已經穿越了第1個屏障”。其中nextGeneration()方法將會喚醒線程0,線程0繼續從trip.await()處運行,由於已經發生了換代,因此直接返回1,最後輸出“Thread0-已經穿越了第1個屏障”

這個時候,線程0和線程1都已經穿越了第一層屏障,當再次調用await()方法時,將會進行第二次換代。


CyclicBarrier與CountDownLatch的區別

對CountDownLatch不熟悉的同學,可以先參考我的另外一篇文章CountDownLatch實現原理

CountDownLatch,是一個線程或多個線程等待另外多個線程執行完畢之後才執行。內部維護一個計數器,每個線程調用一次countDown後,計數器減1,計數器減爲0後,會喚醒因調用await()而阻塞的線程。

CyclicBarrier,多個線程互相等待,直到所有的線程都達到屏障點,纔可以一起接着執行。同樣可以理解爲內部有一個可重置的計數器,每個線程調用await()後,計數器減1,若計數器的值不爲0,將會阻塞該線程。當最後一個線程調用await()後,計數器爲0,將會喚醒所有阻塞的線程,並開啓下一代,重置此計數器的值。

當然兩者也有共同點,調用對應的await()方法都會阻塞當前線程。

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