標籤:java android concurrent 多線程 並發
CountDownLatch
CountDownLatch允許一條或者多條線程等待直至其它線程完成以系列的操作的輔助同步器。
用一個指定的count值對CountDownLatch進行初始化。await方法會阻塞,直至因為調用countDown方法把當前的count降為0,在這以後,所有的等待線程會被釋放,並且在這以後的await調用將會立即返回。這是一個一次性行為——count不能被重設。如果你需要一個可以重設count的版本,考慮使用CyclicBarrier。
其實本類實現非常簡單,和ReentrantLock類似,公有的方法都是調用內部類的橋接模式,內部類是繼承AQS的鎖實現。具體如下:
private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); } int getCount() { return getState(); } protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } protected boolean tryReleaseShared(int releases) { for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } } 建構函式調用setState把count值設定為當前的狀態。內部類Sync由CountDownLatch建構函式時建立,當然保證非負值也是這裡判斷,
public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); } 另外,看看CountDownLatch的的await和countDown方法的實現:
public void countDown() { sync.releaseShared(1); } public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } 可以看到await調了AQS的acquireSharedInterruptibly嘗試擷取共用鎖定,countDown方法調用了releaseShared嘗試釋放共用鎖定,兩個方法的參數都是1,因此當此時有多條線程同時調用await時,這時候看到內部Sync類的tryAcquireShared方法實現,由於在建構函式裡已經調用setState把當前鎖狀態數設定為count,因此這裡在getState()的判斷會一直返回count的值,因此tryAcquireShared會一直返回-1,然後這些調用await的線程都會進入等待隊列。
繼續看看countDown的實現,調用的是AQS的releaseShared的方法,經過調用來到內部Sync類的tryReleaseShared方法,明顯看到方法只是利用自旋把當前的鎖狀態數減去一,直到鎖狀態數等於0,然後返回true,這樣就AQS就把先前進入等待隊列裡的所有等待共用鎖定的線程喚醒,同時如果後面繼續有線程調用await的話,由於鎖狀態數已經變為0,因此tryAcquireShared會一直返回1,這時函數countDown就會立刻返回,不需要再進入等待隊列。
需要注意的是,這裡的count值在建構函式就已經被決定了,後續也沒有方法可以修改,當然這是由這個類的最初設計意圖所決定的。如果需要能夠對count值變更,可以參考CyclicBarrier。
CyclicBarrier
CyclicBarrier允許一組線程互相等待直到一個公平屏障點(common barrier point)。與CountDownLatch不同的是CyclicBarrier著重與互相等待,並且添加重設原狀態的方法。
在涉及一組固定大小的線程的程式中,這些線程必須不時地互相等待,此時CyclicBarrier很有用。因為該屏障(barrier)在釋放等待線程後可以重用,所以稱它為迴圈的屏障。CyclicBarrier支援一個可選的Runnable命令,在一組線程中的最後一個線程到達之後(但在釋放所有線程之前),該命令只在每個屏障點運行一次。若在繼續所有參與線程之前更新共用狀態,此屏障操作很有用。
如果屏障操作不依賴於線程組執行時被懸掛,則線程組內任何線程在被釋放的時候都可以執行屏障操作。為了改進這個行為,每個await的調用都會返回線程到達屏障的索引。然後你就可以選擇哪條線程應該執行屏障操作,例如:
if (barrier.await() == 0) { // 執行屏障操作 } CyclicBarrier對於失敗的同步嘗試,會使用全有或者全無的破壞模型(breakage model):如果一條線程由於中斷、異常或者逾時提前離開了屏障點,其它所有在屏障點等待的線程也會通過拋出BrokenBarrierException(或者InterruptedException異常,同時被中斷的情況下)離開屏障點。接下來看看具體的實現。
首先來看看CyclicBarrier類的建構函式和成員變數以及內部類Generation:
private static class Generation { boolean broken = false; } CyclicBarrier聲明一個內部類Generation,在每次屏障點的使用就代表著一個Generation執行個體。當屏障點被破壞或者重設的時候,generation就要改變。
//保護屏障點入口的鎖 private final ReentrantLock lock = new ReentrantLock(); //使線程在await中等待直至屏障點被脫落(tripped)的條件對象 private final Condition trip = lock.newCondition(); //需要調用await來脫落屏障點的線程數,為final變數 private final int parties; //在屏障點被脫落之後需要啟動並執行命令 private final Runnable barrierCommand; //當前的Generation private Generation generation = new Generation(); //在每次generation中從parties遞減到0,當新的Generation被建立或者屏障點被破壞的時候,count就會被重設 private int count;
成員變數中採用lock和trip進行同步控制,另外parties和count記錄線程數,generation則表示屏障點的目前狀態,還有barrierCommand記錄屏障點破壞後需要執行的命令。
public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction; } 其中parties表示在屏障點被脫落(tripped)之前需要調用await的線程數,需要注意的是parties是final變數,因此不能改變,而成員變數count則在每次await的時候遞減,重設的時候把parties賦值即可。barrierAction表示當屏障點被脫落的時候,執行的命令。要注意的時parties數在建構函式裡設定以後就不能更改,如果屏障被脫落的時候,可以調用reset重設,我們先來看看await的實現。在具體實現裡,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)); } 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(); } int index = --count; if (index == 0) { // tripped boolean ranAction = false; try { final Runnable command = barrierCommand; if (command != null) command.run(); ranAction = true; nextGeneration(); return 0; } finally { if (!ranAction) breakBarrier(); } } 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 { 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(); } } 可以看到無逾時版本和逾時版本的await實現只是在調用dowait函數的參數不同。具體來看看dowait的實現。和之前解析的ReentranLock,ReentrantReadWriteLock,以及上面的CountDownLatch不同,CyclicBarrier並沒有重載AQS類,而是選擇了直接使用ReentrantLock以及其Condition。
dowait函數一開始就調用RenntrantLock的lock方法嘗試擷取鎖,當擷取鎖成功之後,然後建立棧變數g保留成員變數generation,由於generation在後面會被其它線程重新賦值,因此用棧變數保留的做法在多線程同步是實用的技巧。接著檢驗當前Generation的broken變數,如果該變數為true(在重設或者有足夠線程調用類await破壞了屏障點),則此刻馬上拋出BrokenBarrierException異常,然後檢查當前線程是否已經被中斷,如果被中斷則調用breakBarrier,根據CyclicBarrier的特性,當一個await的線程中斷,則屏障點被破壞,則所有await的線程被喚醒拋出異常。
private void breakBarrier() { generation.broken = true; count = parties; trip.signalAll(); } breakBarrier作用就是破壞屏障點,函數首先把成員變數的generation.broken變為true,重設count為parties值,然後調用條件對象trip的signalAll喚醒所有在await等待的線程。在await下面我們即將會看到,breakBarrier會造成之前在await的所有線程拋出異常。如果線程沒有被中斷,則把count自減,並保留至index(count同樣會在後面釋放鎖的時候被其它線程修改),如果index為0,則表示屏障點已經被破壞,然後如果barrierCommand非null,則執行命令,如果執行成功ranAction修改為true,否則在命令裡拋出任何異常的話,則會在finally塊調用breakBarrier破壞屏障點,喚醒其它線程。命令執行成功後,則會調用nextGeneration(脫落當前的屏障點):
private void nextGeneration() { trip.signalAll(); count = parties; generation = new Generation(); } 函數先喚醒所有在等待中的線程,然後重設count,接著建立一個新的Generation類執行個體,這樣就等於把CyclicBarrier內部的狀態重設。
我們再來看回dowait的實現,如果當前的index還沒有遞減到0,則會進入一個迴圈,在這裡首先調用trip的await函數按照參數進行逾時或無窮等待,如果在等待過程中,有線程拋出了InterruptedException中斷了當前線程,則要繼續判斷generation是否和當前發生改變,如果棧變數g和generation相等,並且broken為false,則表示在await中發生了中斷,因此要調用breakBarrier破壞屏障點,然後拋出異常;但如果以上條件不符合,則表明當前屏障點已經被脫落,但線上程仍在等待喚醒的過程中發生的中斷,則此次中斷應該於當前的await無關,則需要調用Thread.interrupt方法重設interrupt標識。
然後如果是被正常喚醒或者逾時等待await以後,還要繼續判斷g.broken,如果為true,則表示屏障被破壞,要拋出BrokenBarrierException異常;如果棧變數g不和generation相等,則表示當前屏障點已經被脫落,因此要返回之前的index表達進入屏障點的索引。另外繼續是否逾時,如果逾時則同樣需要breakBarrier並且拋出TimeoutException。由於考慮到線程並發問題,如果以上判斷都失敗則必須要重新迴圈。最後離開函數的時候必須要調用lock.unlock釋放鎖。
另外,如果在await的線程數沒有達到parties,但需要重新同步,可以調用reset方法。
public void reset() { final ReentrantLock lock = this.lock; lock.lock(); try { breakBarrier(); // break the current generation nextGeneration(); // start a new generation } finally { lock.unlock(); } } 函數實現很簡單,嘗試擷取鎖,然後調用breakBarrier和nextGeneration方法。這樣調用之後,屏障點就會被破壞(breakage),則把之前在await的線程喚醒並讓它們拋出異常;然後調用nextGeneration重設目前狀態,這樣後來的await能夠再次重新等待。
總結
這樣,我們就完整地把CountDownLatch和CyclicBarrier進行了分析。CountDownLatch著重於多組線程等待另外一組線程完成操作,並且是無法重設的;CyclicBarrier則是著重於一組線程互相等待到對方都完成操作為止,但可以重設。
如果要考慮到CountDownLatch為什麼不提供一個可重設的方法,個人認為考慮到實現重設,則必須要像CyclicBarrier一樣要考慮到對其它正在等待線程的影響,這樣勢必就會使整個同步器模型更加複雜,令使用者不方便,同時也會加大實現難度,這樣不如像JUC包給出的解決方案一樣,提供CountDownLatch用於更普遍的簡單的並發情況,另外再提供CyclicBarrier來為更加複雜的並行存取模型提供協助。而事實上CountDownLatch的同步模型比CyclicBarrier要簡單,主要體現在等待線程之間不會互相影響,另外CountDownLatch的實現也要比CyclicBarrier更加簡單。