在我參閱的眾多的書籍當(dāng)中,都沒有看到對這個(gè)類名的翻譯,可能是覺得沒有一個(gè)更好的中文單詞從字面上描述這個(gè)類名,又或者有合適的但是正確不能表達(dá)它要表達(dá)的含義。
JDK API的書寫者既然用這個(gè)類名,想其他大多數(shù)類名一樣,肯定是希望達(dá)到望文知義的效果。現(xiàn)在我們試圖從這個(gè)命名出發(fā)來描述它要表達(dá)的含義。
CountDownLatch = count down + latch
count down: 計(jì)數(shù)減小
latch: 門閂——指門關(guān)上后,插在門內(nèi)使門推不開的滑動(dòng)插銷。
幾乎所有的文章都把我這里的"計(jì)數(shù)減小"描述成"倒計(jì)時(shí)"。不過因?yàn)槲覀兊某WR(shí),“倒計(jì)時(shí)”跟時(shí)間有關(guān)系,我們所看到的"倒計(jì)時(shí)"是自動(dòng)地,可能就會(huì)覺得這個(gè)CountDownLatch類也有這種自動(dòng)倒計(jì)時(shí)的功能,有這樣自覺地話對我們理解和使用這個(gè)類就有影響,所以這里我不把它寫成倒計(jì)時(shí)。
那么從字面上翻譯就是——計(jì)數(shù)減小門閂。
這個(gè)看上去很生硬,的確是這樣。
那么我們可不可以這樣望文生義一下:這個(gè)CountDownLatch提供了兩個(gè)功能,一個(gè)就是計(jì)數(shù)減小功能,另一個(gè)就是門閂功能。
計(jì)數(shù)減小功能:對于這個(gè)功能,相信大多數(shù)人都是很清楚如何實(shí)現(xiàn)的。比如說倒計(jì)時(shí)60秒,那么第一步首先規(guī)定有60秒,第二步開始不斷地減秒數(shù)。
門閂功能:門閂的作用是關(guān)住門,阻止別人再進(jìn)來。關(guān)注門是要做的動(dòng)作,阻止別人再進(jìn)來是目的。
有的人覺得還應(yīng)該有一個(gè)打開門閂的功能,我們暫且猜測,這是個(gè)智能門閂,計(jì)數(shù)變?yōu)?之后門閂自動(dòng)打開。
通過研究和使用CountDownLatch類,我們發(fā)現(xiàn)它提供的功能與我們猜測相符。
計(jì)數(shù)總數(shù)設(shè)置
CountDownLatch通過提供可接受倒計(jì)時(shí)總數(shù)作為參數(shù)的構(gòu)造方法實(shí)現(xiàn)倒計(jì)時(shí)總數(shù)設(shè)置。這個(gè)計(jì)數(shù)總數(shù)通常都是具有某種業(yè)務(wù)含義的數(shù)字。
public CountDownLatch(int count)
計(jì)數(shù)減數(shù)
下面的方法提供計(jì)數(shù)減數(shù)的功能,每次減1。
public void countDown()
上門閂
public void await()
現(xiàn)在我們通過源碼來逐個(gè)分析上面的功能。
計(jì)數(shù)總數(shù)設(shè)置
上面提到了是通過CountDownLatch的構(gòu)造方法設(shè)置的,這里因?yàn)樯婕暗紸QS的知識(shí),所以我們不深入源碼,只要知道這個(gè)類擁有一個(gè)表示計(jì)數(shù)的屬性。
private volatile int state;
計(jì)數(shù)減數(shù)
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
在我們上面設(shè)置的計(jì)數(shù)總數(shù)的基礎(chǔ)上減1,計(jì)數(shù)總數(shù)變?yōu)闇p1后的值。
上門閂
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
這個(gè)方法會(huì)檢查當(dāng)前的計(jì)數(shù)是否為0,如果計(jì)數(shù)不為0那么就表示上門閂成功,await()后的代碼被門閂擋住,無法繼續(xù)執(zhí)行,當(dāng)前的線程會(huì)被掛起,直到計(jì)數(shù)為0,線程被喚醒繼續(xù)執(zhí)行。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
打開門閂
上面我們提到"打開門閂"這個(gè)功能,而且我們猜測是自動(dòng)的,看來確實(shí)是這樣,因?yàn)镃ountDownLatch這個(gè)類沒有為我們提供打開門閂這個(gè)方法。
現(xiàn)在我們來研究一下,什么時(shí)候會(huì)打開門閂。
在講到"上門閂"這個(gè)功能的時(shí)候,我們提到了上門閂后線程就被掛起了,等待被喚醒。那么就看一下什么時(shí)候喚醒。
我們知道計(jì)數(shù)的減少是通過countDown這個(gè)方法來控制的,它對計(jì)數(shù)這個(gè)值是很敏感的,因?yàn)槊空{(diào)一次它會(huì)獲取這個(gè)計(jì)數(shù)進(jìn)行減1,也就是說當(dāng)計(jì)數(shù)變?yōu)?的時(shí)候是它觸發(fā)的,那么這個(gè)時(shí)候它很適合喚醒上門閂的線程。研究代碼發(fā)現(xiàn),的確如此,下面是從代碼的角度對這段邏輯的分析。
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
tryReleaseShared的代碼中我們可以看到nextc表示計(jì)數(shù)減1后的值,如果計(jì)數(shù)減1后為0則方法會(huì)返回true。這個(gè)返回結(jié)果為ture之后,releaseShared的下面這段方法就會(huì)執(zhí)行:
doReleaseShared();
這段代碼我們不深究(因?yàn)樯婕暗紸QS的知識(shí)),我們只要知道它的功能就是喚醒上門閂的那個(gè)線程。
現(xiàn)在我們可以這樣總結(jié)了:也就是說不斷調(diào)用countDown方法,等到計(jì)數(shù)總數(shù)變成0之后 ,上門閂的那個(gè)線程就被喚醒了,門閂就被打開了,就可以繼續(xù)執(zhí)行門閂后面的代碼。
現(xiàn)在通過下面的場景來看看CountDownLatch的使用。
學(xué)生春游場景
場景描述:現(xiàn)在你們班要去春游,準(zhǔn)備做大巴車去,只有等所有的同學(xué)都上車之后,司機(jī)才會(huì)開車出發(fā)。
老師拿了個(gè)包含50個(gè)同學(xué)名字的名單,同學(xué)來一個(gè)就劃掉一個(gè),當(dāng)所有的同學(xué)都被劃掉后,說明所有的同學(xué)都到了,這時(shí)候就可以出發(fā)了。
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
public class SpringOuting {
public static void main(String[] args) throws Exception {
CountDownLatch cd = new CountDownLatch(50);// 學(xué)生名單
// 司機(jī)
new Thread(new Runnable() {
@Override
public void run() {
try {
cd.await();// 等待所有的學(xué)生從名單中被劃掉
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("司機(jī)啟動(dòng)車出發(fā)....");
}
}, "司機(jī)").start();
// 學(xué)生們
Set<Thread> hashSet = new HashSet<>();
for (int i=1; i<=50; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "上車了...");
cd.countDown();// 從名單中劃掉
}
}, "同學(xué)" + i);
hashSet.add(t);
}
Iterator<Thread> it = hashSet.iterator();
while (it.hasNext()) {
Thread t = it.next();
t.start();
Thread.sleep(1000);
}
}
}
這里說個(gè)題外話,是關(guān)于sleep的,寫這個(gè)類的時(shí)候我想模擬同學(xué)一個(gè)一個(gè)上車的效果,當(dāng)時(shí)并不是在代碼最后一行這里加上sleep語句的,而是在springOuting.getOn()這個(gè)的上面加上sleep,測試發(fā)現(xiàn)沒有效果。經(jīng)過分析得到了原因:迭代這里啟動(dòng)線程,50個(gè)線程可以說是一瞬間啟動(dòng)了,雖然CPU每個(gè)時(shí)刻只有一個(gè)線程占用(假設(shè)單核),但是它切換線程足夠得快,使得這50個(gè)線程幾乎同一時(shí)間執(zhí)行到sleep代碼,這樣50個(gè)線程幾乎同時(shí)都休眠了,然后幾乎同一時(shí)間休眠結(jié)束,所以就把sleep加到表示同學(xué)的線程的內(nèi)部是不起作用的。
總結(jié)
最后通過JDK API的描述來說明這個(gè)類的功能:
CountDownLatch這個(gè)類能夠使一個(gè)線程等待其他線程完成各自的工作后再執(zhí)行。
應(yīng)用場景
此為《Java并發(fā)編程的藝術(shù)》提到的一個(gè)場景:我們需要解析一個(gè)Excel里多個(gè)sheet的數(shù)據(jù),此時(shí)可以考慮使用多線程,每個(gè)線程解析一個(gè)sheet里的數(shù)據(jù),等到所有的sheet都解析完之后,程序需要提示解析完成。在這個(gè)需求中,要實(shí)現(xiàn)主線程等待所有線程完成sheet的解析操作。
實(shí)現(xiàn)原理
具體原理參考:Java并發(fā)編程 - 共享鎖
簡要說明:線程執(zhí)行await判斷內(nèi)部令牌數(shù)是否為0,如果不為0,當(dāng)前線程就會(huì)被放入同步隊(duì)列中;其他線程執(zhí)行完自己的后調(diào)用countDown釋放1個(gè)令牌,當(dāng)最后一個(gè)調(diào)用的線程通過countDown把令牌數(shù)降至為0后,執(zhí)行同步隊(duì)列中的線程喚醒操作喚醒線程(多個(gè)線程await,由于喚醒的傳播性,則都會(huì)被喚醒)。