JAVA concurrency -- CyclicBarrier 與 CountDownLatch 源碼詳解

JAVA concurrency -- CyclicBarrier 與 CountDownLatch 源碼詳解

概述
CountDownLatch和CyclicBarrier有着相似之處,並且也常常有人將他們拿出來進行比較,這次,筆者試着從源碼的角度分別解析這兩個類,並且從源碼的角度出發,看看兩個類的不同之處。

CountDownLatch
CountDownLatch從字面上來看是一個計數工具類,實際上這個類是用來進行多線程計數的JAVA方法。

CountDownLatch內部的實現主要是依靠AQS的共享模式。當一個線程把CountDownLatch初始化了一個count之後,其他的線程調用await就會阻塞住,直到其他的線程一個一個調用countDown方法進行release操作,把count的值減到0,即把同步鎖釋放掉,await纔會進行下去。

Sync
內部主要還是實現了一個繼承自AQS的同步器Sync。Sync源碼如下:

private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;

    // 構造方法,參數是count的數值
    Sync(int count) {
        // 內部使用state來存儲count
        setState(count);
    }

    // 獲取count的值
    int getCount() {
        return getState();
    }

    // 嘗試獲取分享模式同步器
    protected int tryAcquireShared(int acquires) {
        // 判斷state的值,如果爲0則獲取成功,否則獲取失敗
        // 繼承自AQS,根據AQS中的註釋我們可以知道如果返回結果
        // 大於0則說明獲取成功,如果小於0則說明獲取失敗
        // 此處不會返回0,因爲沒有意義
        return (getState() == 0) ? 1 : -1;
    }

    // 釋放同步器
    protected boolean tryReleaseShared(int releases) {
        // 自選操作
        for (;;) {
            // 獲取state
            int c = getState();
            // 如果state爲0,直接返回false
            if (c == 0)
                return false;
            // 計算state-1的結果
            int nextc = c-1;
            // CAS操作將這個值同步到state上
            if (compareAndSetState(c, nextc))
                // 如果同步成功,則判斷是否此時state爲0
                return nextc == 0;
        }
    }
}

Sync是繼承自AQS的同步器,這段代碼中值得拿出來討論的有以下幾點:

爲什麼用state來存儲count的數值?
因爲state和count其實上是一個概念,當state爲0的時候說明資源是空閒的,當count爲0時,說明所有的CountDownLatch線程都已經完成,所以兩者雖然說不是同樣的意義,但是在代碼實現層面的表現是完全一致的,因此可以將count記錄在state中。

爲什麼tryAcquireShared不會返回0?
首先需要解釋下tryAcquireShared在AQS中可能的返回值:負數說明是不可以獲取共享鎖,0說明是可以獲取共享鎖,但是當前線程獲取後已經把所有的共享鎖資源佔完了,接下來的線程將不會再有多餘資源可以獲取了,正數則說明了你可以獲取共享鎖,並且之後還有餘量可以給其他線程提供共享鎖。然後我們回過來看CountDownLatch內部的tryAcquireShared,我們在實現上完全不關注後續線程,後續的資源佔用狀況,我只要當前狀態,那麼這個0的返回值實際上是沒有必要的。

爲什麼tryReleaseShared中的參數不被使用到?
根據這個類的實現方式,我們可以知道tryReleaseShared的參數一定是1,因爲線程的完成一定是一個一個倒數完成的。實際上我們去看countDown方法內部調用到了sync.releaseShared方法的時候可以發現他寫死了參數爲1,所以實際上tryReleaseShared中的參數不被使用到的原因是因爲參數值固定爲1.

構造函數和方法

// 構造方法
public CountDownLatch(int count) {
    // count必須大於0
    if (count < 0) throw new IllegalArgumentException("count < 0");
    // 初始化Sync
    this.sync = new Sync(count);
}
// 等待獲取鎖(可被打斷)
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// 等待獲取鎖(延遲)
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

// 計數器降低(釋放同步器)
// 每次調用減少1
public void countDown() {
    sync.releaseShared(1);
}

// 獲取count
public long getCount() {
    return sync.getCount();
}

// toString
public String toString() {
    return super.toString() + "[Count = " + sync.getCount() + "]";
}

CyclicBarrier
CyclicBarrier從字面上看是循環柵欄,在JAVA中的作用是讓所有的線程完成後進行等待,直到所有的線程全部完成,再進行接下來的操作。

CyclicBarrier並沒有直接繼承AQS實現同步,而是藉助了可重入鎖ReentrantLock以及Condition來完成自己的內部邏輯。

成員變量

// 鎖
private final ReentrantLock lock = new ReentrantLock();

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

// 線程數
private final int parties;

// 執行完所有線程後執行的Runnable方法,可以爲空
private final Runnable barrierCommand;

// 分組
private Generation generation = new Generation();

// 未完成的線程數
private int count;

private static class Generation {
    boolean broken = false;
}

我們可以看到成員變量中有一個很陌生的類Generation,這個是CyclicBarrier內部聲明的一個static類,作用是幫助區分線程的分組分代,使得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);
}

很常規的構造函數,只是簡單的初始化成員變量,沒有特別的地方。

核心方法

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

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

await是CyclicBarrier的核心方法,就是靠着這個方法來實現線程的統一規劃的,其中調用的是內部實現的doWait,我們來看下代碼:

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    // 常規的加鎖操作,至於爲什麼要用本地變量操作,
    // 可以去看下我寫的另一篇ArrayBlockingQueue的相關文章
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 獲取Generation類
        final Generation g = generation;

        // 查看generation是否是broken,如果是broken的,
        // 那說明之前可能因爲某些線程中斷或者是一些意外狀態導致沒有辦法
        // 完成所有線程到達終點(tripped)的目標而只能報錯
        if (g.broken)
            throw new BrokenBarrierException();

        // 如果線程被外部中斷需要報錯,並且在內部需要將
        // generation的broken置爲true來讓其他線程能夠感知到中斷
        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }

        // 將線程未完成數減1
        int index = --count;
        // 如果此時剩餘線程數爲0,則說明所有的線程均已完成,即到達tripped狀態
        if (index == 0) {
            boolean ranAction = false;
            try {
                // 如果有預設完成後執行的方法,則執行
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                // 此時由於這一個輪迴的線程已經全部完成,
                // 所以調用nextGeneration方法開啓一個新的輪迴
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // 如果此時還有其他的線程未完成,則當前線程開啓自旋模式
        for (;;) {
            try {
                if (!timed)
                    // 如果timed爲false,trip則阻塞住直到被喚醒
                    trip.await();
                else if (nanos > 0L)
                    // 如果timed爲true,則調用awaitNanos設定時間
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    Thread.currentThread().interrupt();
                }
            }

            // 查看generation是否是broken,如果是broken的拋出異常
            if (g.broken)
                throw new BrokenBarrierException();

            // 如果g != generation意味着generation
            // 已經被賦予了一個新的對象,這說明要麼是所有線程已經完成任務開啓下一個輪迴,
            // 要麼是已經失敗了,然後開啓的下一個輪迴,無論是哪一種情況,都return
            if (g != generation)
                return index;

            // 如果已經超時,則強制打斷
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

看完這段核心代碼之後我們回頭再來反思Generation的意義,我們已經可以大致的給出使用Generation的理由了:

不同於CountDownLatch的實現,CyclicBarrier採取了更加複雜的方式,原因便是因爲內部涉及到了多線程之間的干預與通信,CountDownLatch不關心線程的實現與進程,他只是一個計數器,而CyclicBarrier則需要知道線程是否正常的完結,是否被中斷,如果用其他的方式代價會比較高,因此,CyclicBarrier的作者通過靜態內部類的方式將整個分代的狀態共享於多個線程之間,保證每個線程能夠獲取到柵欄的狀態以及能夠將自身的狀態更好的反饋回去。同時,這種方式便於重置,也使得CyclicBarrier可以高效的重用。至於爲什麼broken沒有用volatile修飾,因爲類的方法內部全部都上了鎖,所以不會出現數據不同步的問題。

總結
CountDownLatch和CyclicBarrier從使用上來說可能會有一些相似之處,但是在我們看完源碼之後我們會發現兩者可以說是天差地別,實現原理,實現方式,應用場景均不相同,總結下來有以下幾點:

CountDownLatch實現直接依託於AQS;CyclicBarrier則是藉助了ReentrantLock以及Condition
CountDownLatch是作爲計數器存在的,因此採取了討巧的設計,源碼結構清晰並且簡單,同樣功能也較爲簡單;CyclicBarrier則爲了實現多線程的掌控,採用了比較複雜的設計,在代碼實現上也顯得比較彎彎繞繞。
由於CyclicBarrier採用的實現方式,相比一次性的CountDownLatch,CyclicBarrier可以多次重複使用
計數方式的不同:CountDownLatch採用累加計數, CyclicBarrier則使用倒數計數
原文地址https://my.oschina.net/bjwzds/blog/3534835

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