1.简介
在这篇文章中,我们介绍了一下 CountDownLatch类,并且演示了一下在实战案例中是如何使用的。关键地是,通过使用CountDownLatch,我们可以让一个线程阻塞直到其他线程完成了给定的任务。
2.在并发编程中的使用
简单地说,CountDownLatch有一个counter域,在我们要求的时候,你可以消减这个域。 之后,我们使用它来阻塞一个调用线程直到它被消减为零。如果我们正在做一些并行处理,那么我们就可以把CounterDownLatch的counter的值设置成和我们将要使用的线程数量一样。那时,我们就可以在每个线程结束之后调用countDown()方法,从而保证:一个调用await()的独立线程将会阻塞掉直到工作线程结束。
3.等待线程池完成
在下面的案例中,我们创建了一个Worker并且在该线程运行完成的时候使用CountDownLatch域字段发出一个信号:
publicclassWorker implementsRunnable {
privateList outputScraper;
privateCountDownLatch countDownLatch;
publicWorker(List outputScraper, CountDownLatch countDownLatch) {
this.outputScraper = outputScraper;
this.countDownLatch = countDownLatch;
}
@Override
publicvoidrun() {
doSomeWork();
outputScraper.add("Counted down");
countDownLatch.countDown();
}
}
然后,我们创建一个test来证明一下: 我们可以用一个CountDownLatch来等待Worker实例完成结束:
@Test
publicvoidwhenParallelProcessing_thenMainThreadWillBlockUntilCompletion()
throwsInterruptedException {
List outputScraper = Collections.synchronizedList(newArrayList<>());
CountDownLatch countDownLatch = newCountDownLatch(5);
List workers = Stream
.generate(() -> newThread(newWorker(outputScraper, countDownLatch)))
.limit(5)
.collect(toList());
workers.forEach(Thread::start);
countDownLatch.await();
outputScraper.add("Latch released");
assertThat(outputScraper)
.containsExactly(
"Counted down",
"Counted down",
"Counted down",
"Counted down",
"Counted down",
"Latch released"
);
}
正常情况下,“Latch released” 都将是最后一个输出-因为它依赖于CountDownLatch释放。
注意: 如果我们没有调用await()方法,那就无法保证线程的执行顺序,因此,这个测试也会随机性地失败。
4.一池子的线程等待开始
我们继续拿之前的例子来说,但是这一次,我们却要开启上千个线程而不再是5个。很可能会出现这样的情况,在我们为后面的线程调用start方法之前,前面的许多线程已经结束运行了。
这样的话,想重现一个并发问题就变得很困难了。因为,我们无法让我们的所有线程并发运行。
围绕这一点,我们使用CountdownLatch时,就和之前的例子有点不同了。相比较于 阻塞一个父线程直到一些子线程结束运行,我们可以在所有其他线程开始之前,把每一个子线程阻塞掉。我们可以阻塞每一个子线程直到其他所有线程都启动了(started)。
我们来修改一下run()方法,让他在执行之前先阻塞住:
public class WaitingWorker implementsRunnable {
privateList outputScraper;
private CountDownLatch readyThreadCounter;
private CountDownLatch callingThreadBlocker;
private CountDownLatch completedThreadCounter;
public WaitingWorker(
List outputScraper,
CountDownLatch readyThreadCounter,
CountDownLatch callingThreadBlocker,
CountDownLatch completedThreadCounter) {
this.outputScraper = outputScraper;
this.readyThreadCounter = readyThreadCounter;
this.callingThreadBlocker = callingThreadBlocker;
this.completedThreadCounter = completedThreadCounter;
}
@Override
public void run() {
readyThreadCounter.countDown();
try{
callingThreadBlocker.await();
doSomeWork();
outputScraper.add("Counted down");
} catch(InterruptedException e) {
e.printStackTrace();
} finally{
completedThreadCounter.countDown();
}
}
}
现在,我们也修改一下我们的测试,这样,它就会先在所有的worker线程启动之前阻塞住,然后解除阻塞,之后,再次阻塞直到所有的Worker都已经结束:
@Test
public void whenDoingLotsOfThreadsInParallel_thenStartThemAtTheSameTime()
throwsInterruptedException {
List outputScraper = Collections.synchronizedList(newArrayList<>());
CountDownLatch readyThreadCounter = newCountDownLatch(5);
CountDownLatch callingThreadBlocker = newCountDownLatch(1);
CountDownLatch completedThreadCounter = newCountDownLatch(5);
List workers = Stream
.generate(() -> newThread(newWaitingWorker(
outputScraper, readyThreadCounter, callingThreadBlocker, completedThreadCounter)))
.limit(5)
.collect(toList());
workers.forEach(Thread::start);
readyThreadCounter.await();
outputScraper.add("Workers ready");
callingThreadBlocker.countDown();
completedThreadCounter.await();
outputScraper.add("Workers complete");
assertThat(outputScraper)
.containsExactly(
"Workers ready",
"Counted down",
"Counted down",
"Counted down",
"Counted down",
"Counted down",
"Workers complete"
);
}
这个模式对于重现并发bug十分有用,因为它能强制上千个线程以并行的的方式执行业务逻辑。
5.过早结束一个CountDownLatch
有时,会出现这样的情况,在递减CountDownLatch的值之前,Worker就已经因发生错误而终止。这就会导致这样一个结果:countDownLatch的值永远不会为零,await()方法也决不会结束:
@Override
publicvoidrun() {
if(true) {
thrownewRuntimeException("Oh dear, I'm a BrokenWorker");
}
countDownLatch.countDown();
outputScraper.add("Counted down");
}
为了演示await()方法是如何永远阻塞的,我们用一个BrokenWorker来修改我们之前的例子:
@Test
public void whenFailingToParallelProcess_thenMainThreadShouldGetNotGetStuck()
throws InterruptedException {
List outputScraper = Collections.synchronizedList(newArrayList<>());
CountDownLatch countDownLatch = newCountDownLatch(5);
List workers = Stream
.generate(() -> newThread(newBrokenWorker(outputScraper, countDownLatch)))
.limit(5)
.collect(toList());
workers.forEach(Thread::start);
countDownLatch.await();
}
很明显,这并不是我们想要的行为- 相比较于无限的阻塞,让应用程序继续运行可能更好一点。为了回避这个问题,我们可以为我们的await()方法增加一个超时时间参数timeout:
booleancompleted = countDownLatch.await(3L, TimeUnit.SECONDS);
assertThat(completed).isFalse();
正如我们所看到的,测试最终将超时 ,并且await()将返回false。
6.总结
在这篇短文中,我们论证了我们如何用CountDownLatch来阻塞一个线程直到其他线程结束了一些处理。同时,我们也展示了如何使用CountDownLatch来确保线程并发运行,从而帮助于调试一些并发问题。这些案例代码都能在github上找到,这是一个基于maven的项目,所以运行起来应该很简单,sourceCode