本文内容如有错误、不足之处,欢迎技术爱好者们一同探讨,在本文下面讨论区留言,感谢。
简介
CountDownLatch 在Java中是一种同步器,它允许一个线程在开始执行之前,等待一个或多个线程。
可以在程序中使用Java中的等待和通知机制来实现和CountDownLatch相同的功能 ,但是它需要大量代码,并且在第一次使用时非常困难(tricky),而使用CountDownLatch 可以使用几行代码简单完成。CountDownLatch 还允许灵活地等待主线程要等待的线程数,它可以等待一个线程或n个线程,代码上没有太大变化。关键是需要明白Java应用程序在哪里使用CountDownLatch更好。
例如,应用程序的主线程要等待,直到负责启动框架服务的其他服务线程完成了所有服务的启动。
原理
CountDownLatch的工作原理是使用线程数初始化计数器,每次线程执行完成时,计数器都会递减。当计数器个数(count)达到零时,表示所有线程已完成其执行,并且等待latch锁的线程(例如:主线程)将恢复执行。
从图片上可以看到,流程是TA线程调用其他三个线程,等待其他3个线程执行完成后,才执行TA线程剩下的逻辑。
过程如下
- 主线程启动
- 创建包含N个线程的CountDownLatch
- 启动N个线程
- 主线程等待N个线程执行完毕
- N个线程完成返回
- 主线程恢复执行
使用
CountDownLatch.java类的构造函数:
// 根据count创造初始化一个CountDownLatch
public CountDownLatch(int count) {...}
此计数count本质上是CountDownLatch 应等待的线程数。该值只能设置一次,并且CountDownLatch 没有提供其他机制来重置此count。
使用CountDownLatch 时需要注意的两点是:
- 一旦计数达到零,就不能重用CountDownLatch ,这是CountDownLatch和CyclicBarrier之间的主要区别。
- 主线程通过调用 CountDownLatch.await()方法来等待latch线程完成,而其他线程则调用CountDownLatch.countDown()来通知它们已完成。
例子:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo
{
public static void main(String args[])
throws InterruptedException
{
// 创建一个线程等待其他4个线程执行完毕后再执行
CountDownLatch latch = new CountDownLatch(4);
// 创建并启动4个线程
Worker first = new Worker(1000, latch,
"WORKER-1");
Worker second = new Worker(2000, latch,
"WORKER-2");
Worker third = new Worker(3000, latch,
"WORKER-3");
Worker fourth = new Worker(4000, latch,
"WORKER-4");
first.start();
second.start();
third.start();
fourth.start();
// main-task 等待上面4个线程
latch.await();
// main-thread 开始工作
System.out.println(Thread.currentThread().getName() +
" 已经完成");
}
}
// 资源类
class Worker extends Thread
{
private int delay;
private CountDownLatch latch;
public Worker(int delay, CountDownLatch latch,
String name)
{
super(name);
this.delay = delay;
this.latch = latch;
}
@Override
public void run()
{
try
{
Thread.sleep(delay);
latch.countDown();
System.out.println(Thread.currentThread().getName()
+ " 完成");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
使用CountDownLatch 模拟并发错误出现的场景
实际项目中启动了数千个线程,而不是四个线程,那么许多较早执行的线程可能已经完成处理,甚至后面的线程还没有调用start()方法的时候。这可能使尝试重现并发问题变得困难,因为无法使所有线程并行运行。
为了解决这个问题,使CountdownLatch 的工作方式与前面的示例不同。除了在某些子线程完成之前阻塞父线程之外,需要在每个子线程都启动之前阻塞每个子线程。等所有线程都到位并进行等待的时候,释放这些等待的线程,这就好比1000个人参加100米跑步,所有人都站在起跑线上等待起跑枪声,枪声一响,所有运动员开始比赛,并发跑步。
修改run() 方法,使其在处理之前阻塞:
public class WaitingWorker implements Runnable {
// 输出黑板
private List<String> outputScraper;
// 准备线程数latch
private CountDownLatch readyThreadCounter;
// 调用线程数latch
private CountDownLatch callingThreadBlocker;
// 计算完成线程数latch
private CountDownLatch completedThreadCounter;
public WaitingWorker(
List<String> 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();
}
}
}
下面的测试类,main方法阻塞直到所有Workers启动,然后解除阻塞,然后再阻塞直到Workers完成:
@Test
public void whenDoingLotsOfThreadsInParallel_thenStartThemAtTheSameTime()
throws InterruptedException {
// ArrayList是线程不安全类,需要调用Collections.synchronizedList()进行线程安全处理。
List<String> outputScraper = Collections.synchronizedList(new ArrayList<>());
CountDownLatch readyThreadCounter = new CountDownLatch(4);
CountDownLatch callingThreadBlocker = new CountDownLatch(1);
CountDownLatch completedThreadCounter = new CountDownLatch(4);
// Stream流,生成4个线程数
List<Thread> workers = Stream
.generate(() -> new Thread(new WaitingWorker(
outputScraper, readyThreadCounter, callingThreadBlocker, completedThreadCounter)))
.limit(4)
.collect(toList());
workers.forEach(Thread::start);
readyThreadCounter.await();
outputScraper.add("Workers ready");
// 启动所有线程操作,将调用线程latch进行countDown解除所有线程等待,相当于跑步时的裁判发出的枪声
callingThreadBlocker.countDown();
completedThreadCounter.await();
outputScraper.add("Workers complete");
assertThat(outputScraper)
.containsExactly(
"Workers ready",
"Counted down",
"Counted down",
"Counted down",
"Counted down",
"Workers complete"
);
}
这种方法可以用来迫使成千上万的线程尝试并行执行某些逻辑,因此这种模式对于尝试重现并发错误非常有用。
超时调用处理
上面的代码,可以看到对中断异常进行了try-catch,因为有些线程会发生中断或者其他异常,如果某个线程的异常如下:
@Override
public void run() {
if (true) {
throw new RuntimeException("Oh dear, I'm a BrokenWorker");
}
countDownLatch.countDown();
outputScraper.add("Counted down");
}
那么,调用这个latch的线程将一直进行等待,无法继续执行下去,因为CountDownLatch 中的计数器Counter永远不可能为0,因此需要在await()的调用中添加一个超时参数。
boolean completed = countDownLatch.await(3L, TimeUnit.SECONDS);
使用场景
- 实现最大并行测试,上面的例子中已经给出。
- 等待N个线程完成,然后再开始执行。
- 死锁检测,一个非常方便的用例,可以在每个测试阶段使用N个线程访问具有不同数量线程的共享资源,试图创建死锁进行测试。
参考资料
How is CountDownLatch used in Java Multithreading? (在Java多线程中如何使用CountDownLatch?)
Guide to CountDownLatch in Java (CountDownLatch指南)
CountDownLatch in Java (Java中的CountDownLatch)
Java concurrency – CountDownLatch Example(CountDownLatch案例)