關注王有志,一個分享硬核Java技術的互金摸魚俠
歡迎你加入Java人的提桶跑路群:共同富裕的Java人
今天我們來聊一聊AQS家族中的另一個重要成員CountDownLatch。關於CountDownLatch的面試題並不多,除了問「是什麼」和「如何實現的「外,CountDownLatch還會和CyclicBarrier進行對比:
什麼是CountDownLatch?它是如何實現的?
CountDownLatch和CyclicBarrier有什麼區別?
按照慣例,我們依舊是按照「是什麼」,「怎麼用」和「如何實現的」這3步來分析CountDownLatch,至於與CyclicBarrier的差異,下一篇我們再詳細分析。
Tips:今天的「是什麼」和「怎麼用」合併了。
不知道你有沒有參加過那種感動老闆,並伴以「提升」組織凝聚力為主旨的公司團建?通常行政會組織一場越野徒步活動,規定每個人都到達終點後才能吃飯,美名其曰「不拋棄不放棄的團隊精神」。而老闆會早早的在終點拿著花名冊等待,當員工到達終點後,在花名冊上劃掉自己的名字,當最後一名員工到達終點後,還要敲響鑼鼓,告知老闆可以開始下一輪的折磨了。
那麼這樣一場越野徒步活動就可以用CountDownLatch來進行簡單的程式碼描述:
CountDownLatch countDownLatch = new CountDownLatch(10);
// 10個人進行越野徒步
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
// 每個人比前一個選手晚1秒
TimeUnit.SECONDS.sleep((finalI + 1));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("選手[" + finalI + "]到達終點!!!");
countDownLatch.countDown();
}).start();
}
// 老闆在目的地吃瓜,等待每個選手到達
countDownLatch.await();
// 開飯啦!
System.out.println("老闆說:所有人都到齊了,午飯是每人一個吐司!!!");
看到這裡,參加過此類團建活動的小夥伴是不是血壓有些高了?但是你先別高,因為在這樣一場血壓飆升的團建中,我們已經不知不覺的掌握了CountDownLatch的用法了。
我們先試著從名字來理解CountDownLatch,CountDownLatch是一個組合詞,CountDown譯為「倒計時」,Latch譯為「門閂」,結合起來就是倒計時結束後開啟門閂(進行後續的動作)。再來看Doug Lea是如何解釋CountDownLatch的作用的:
A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
CountDownLatch是一個同步輔助工具,它允許一個或多個執行緒等待其他執行緒完成操作(進而執行後續操作)。
需要注意的是,CountDownLatch允許一個或多個執行緒進入等待,我們只需要在不同的執行緒中呼叫CountDownLatch.await
就可以實現多個執行緒的等待。
先來看作為AQS家族的成員,CountDownLatch是如何與AQS產生聯絡的:
很熟悉的結構,與ReentrantLock和Semaphore一樣,都是內部的同步器類Sync
繼承了AQS,但不同的是CountDownLatch中的Sync
不再是抽象類。
既然是繼承自AQS,並且內部有計數器(倒計數也是計數)的使用,那麼我們就再次搬出《AQS的今生,構建出JUC的基礎》中那段關於同步狀態作為計數器特性的說明:
AQS中,state不僅用作表示同步狀態,也是某些同步器實現的計數器,如:
Semaphore
中允許通過的執行緒數量,ReentrantLock
中可重入特性的實現,都依賴於state
作為計數器的特性。
雖然沒有舉CountDownLatch的例子,但我知道在經過Semaphore的分析後你一定能夠猜到CountDownLatch是如何使用同步狀態作為計數器特性的。接下來我們就一起來看一下同步狀態在CountDownLatch中的應用。
通過AQS家族成員的類圖可以看到,CountDownLatch中的同步器Sync
並沒有公平與非公平的區別,因此構造器只需要提供設定計數的能力即可:
public class CountDownLatch {
public CountDownLatch(int count) {
if (count < 0){
throw new IllegalArgumentException("count < 0");
}
this.sync = new Sync(count);
}
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
protected final void setState(int newState) {
state = newState;
}
}
不出所料,CountDownLatch的計數依舊是迴歸到了AQS的state
上。
回到徒步活動中,員工到達終點後,需要在花名冊上劃掉自己的名字,最後一名到達後還要敲響鑼鼓。在程式碼實現中,我們使用了CountDownLatch.countDown
表示員工到達的狀態,並執行相應的動作:
public class CountDownLatch {
public void countDown() {
sync.releaseShared(1);
}
private static final class Sync extends AbstractQueuedSynchronizer {
protected boolean tryReleaseShared(int releases) {
for (;;) {
// 獲取同步狀態
int c = getState();
// 同步狀態為0,返回失敗
if (c == 0){
return false;
}
// 計數減1,並通過CAS更新
int nextc = c - 1;
if (compareAndSetState(c, nextc)) {
// 計數器為0時返回true
return nextc == 0;
}
}
}
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
}
回憶下《詳解AQS家族的成員:Semaphore》中Semaphore#release
方法的實現,是不是覺得似曾相識?同樣是執行Sync#tryReleaseShared
方法,並在成功後呼叫AQS的doReleaseShared
方法。區別是Semaphore#tryReleaseShared
的實現是計數加1,而CountDownLatch#tryReleaseShared
實現是計數減1。
我們注意另一個問題,CountDownLatch的Sync#tryReleaseShared
方法只有在計數器減為0時才會返回true,此時能進入AQS的doReleaseShared
方法,否則都只是執行了計數器減一的操作。
此外,我們也知道AQS的doReleaseShared
方法起到了喚醒AQS等待佇列中節點的作用,也就是說只有在計數器減為0時,CountDownLatch才會執行一次喚醒工作。
Tips:AQS的doReleaseShared
已經在《詳解AQS家族的成員:Semaphore》中分析過了,就不再贅述了~~
我們知道老闆一早就乘車到達了終點等待,那麼老闆是如何判斷自己要等待呢?老闆提前抵達終點後,拿出花名冊統計到達人數,當發現還有人沒有到達終點時,他就準備打個盹,睡一覺。
我們使用了CountDownLatch.await
表示老闆進入等待狀態:
public class CountDownLatch {
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
if (tryAcquireShared(arg) < 0) {
doAcquireSharedInterruptibly(arg);
}
}
}
是不是還是很眼熟?與Semaphore一樣使用了AQS的acquireSharedInterruptibly
方法,那我們重點關注CountDownLatch的Sync#tryAcquireShared
方法:
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int acquires) {
// 同步狀態為0返回1,不為0返回-1
return (getState() == 0) ? 1 : -1;
}
}
}
該方法對同步狀態做出了判斷,結合AQS的acquireSharedInterruptibly
方法我們可以得到以下結論:
當同步狀態等於0時,tryAcquireShared
返回1,不執行doAcquireSharedInterruptibly
,即執行了足夠次數的countDownLatch#countDown
,無需進入等待佇列;
當同步狀態不等於0時,tryAcquireShared
返回-1,執行doAcquireSharedInterruptibly
,即尚未執行足夠次數的countDownLatch#countDown
,需要進入等待佇列。
簡單來說就是在呼叫CountDownLatch#await
方法時計數器不為0構建等待佇列,為0就什麼也不執行。
Tips:AQS的doAcquireSharedInterruptibly
已經在《詳解AQS家族的成員:Semaphore》中分析過了,就不再贅述了~~
關於CountDownLatch的內容到這裡就結束了,內容並不多。當我們不熟悉AQS的時候,不認識CountDownLatch的時候,會覺得CountDownLatch是一種「挺高階」的工具,但當我們深入其中時就會發現,「高階」的技術其實並不難學。
好了,如果本文對你有幫助的話,還希望你不要吝嗇點贊。最後歡迎大家關注分享硬核技術的金融摸魚俠王有志,以及關注我的專欄《Java面試都問啥?》,我們下次再見!