一、什么是CountDownLatch
CountDownLatch
中count down
是倒数的意思,latch
则是门闩
的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉
。
CountDownLatch
的作用也是如此,在构造CountDownLatch(int count)
:的时候需要传入一个整数count
,在这个整数“倒数”到0
之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。
总结来说,CountDownLatch
的作用就是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,然后主线程才继续往下执行。
二、主要方法
CountDownLatch(int count)
:构造方法,创建一个新的CountDownLatch
实例,用给定的计数初始化。参数 count 表示线程需要等待的任务数量。
int numberOfTasks = 5;
CountDownLatch latch = new CountDownLatch(numberOfTasks);
void await()
:使当前线程等待,直到计数器值变为0,除非线程被interrupted
。如果计数器的值已经为0,则此方法立即返回。在实际应用中,通常在主线程中调用此方法,等待其他子线程完成任务。
latch.await();
boolean await(long timeout, TimeUnit unit)
:使当前线程等待,直到计数器值变为0,或者指定的等待时间已到,或者线程被interrupted
。如果计数器的值已经为0,则此方法立即返回。
参数timeout
是指定的等待时间,
参数unit
是timeout
的单位(如秒、毫秒等)。
此方法返回一个布尔值,表示在等待时间内计数器是否变为0。
latch.await(5, TimeUnit.SECONDS);
这里需要注意的是,await()
方法并没有规定只能有一个线程执行该方法,如果多个线程同时执行await()
方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁。
void countDown()
:递减计数器的值。如果计数器的结果为0, 则释放所有等待的线程。在实际应用中,通常在线程完成任务后调用此方法。
latch.countDown();
这里需要注意的是,countDown()
方法并没有规定一个线程只能调用一次,当同一个线程调用多次countDown()
方法时,每次都会使计数器减一;
long getCount()
:获取当前计数的值。返回当前CountDownLatch
实例内部的计数值。
long remainingCount = latch.getCount();
三、优缺点
- 优点:
- 简化了线程间的通信和同步。在某些并发场景中,需要等待其他线程完成任务后才能继续执行,使用 CountDownLatch 可以简化这种操作,而不需要复杂的锁和等待/通知机制。
- 提高性能。由于 CountDownLatch 可以让线程在完成任务后立即递减计数值,而不需要等待其他线程完成任务,因此可以减少阻塞,提高程序运行性能。
- 支持灵活的计数。可以通过创建不同的 CountDownLatch 实例,实现对多个线程任务计数。
- 缺点:
- 单次使用。CountDownLatch 的计数值无法重置。一旦计数值到达零,它就不能再被使用了。在需要重复使用的场景中,可以选用 CyclicBarrier 或 Semaphore。
- 没有返回值。CountDownLatch 无法获得执行任务的线程所返回的结果。如果需要收集线程执行结果,可以考虑使用
java.util.concurrent.Future
和java.util.concurrent.ExecutorService
。
四、使用场景
- 启动多个线程执行并行任务,主线程等待所有并行任务完成后继续执行。
例如:在测试中,准备数据阶段,需要同时查询多个子系统的数据和处理,等待处理结束后再进行下一步操作。 - 控制线程的执行顺序。一个线程需要等待其他线程的结果或者完成任务后才能继续执行。
例如:一个文件解压缩程序,首先需要下载文件,下载完成后解压文件。 - 实现一个计数器,允许一个或多个线程等待直到计数器为0。这对于在系统初始化时,需要等待资源加载或者初始化的场景十分有用。
例如:等待加载配置文件、启动连接池等操作完成后才开始处理其他任务。
四、示例代码
4.1 示例代码
下面的示例展示了一个简单的网站爬虫,它使用 CountDownLatch 在主线程中等待其他爬虫线程完成任务。
在这个例子中,我们要爬取一组网站的内容,在主线程中等待所有爬虫任务完成。
首先,我们创建一个 URLs 列表,包含多个网站 URL。
然后,我们使用 CountDownLatch 实例 latch
来跟踪待完成的爬虫任务数量。
接着,我们遍历 URL 列表,为每个 URL 创建一个新的 Crawler 线程。Crawler 类实现了 Runnable
接口,用于读取指定 URL 的网页内容。在完成任务后,它调用 latch.countDown()
方法减少计数值。
最后,在主线程中,我们调用 latch.await()
方法等待所有爬虫线程完成任务。当所有任务完成时,打印一条消息表示爬虫任务已完成。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class WebCrawler {
private static class Crawler implements Runnable {
private final String url;
private final CountDownLatch latch;
public Crawler(String url, CountDownLatch latch) {
this.url = url;
this.latch = latch;
}
@Override
public void run() {
try {
URL urlObject = new URL(url);
BufferedReader in = new BufferedReader(new InputStreamReader(urlObject.openStream()));
String inputLine;
StringBuilder content = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
content.append(inputLine);
content.append("\n");
}
in.close();
System.out.println("爬取 " + url + " 成功, 内容大小: " + content.length() + " 字符");
} catch (Exception e) {
System.err.println("爬取 " + url + " 失败, 原因: " + e.getMessage());
} finally {
latch.countDown();
}
}
}
public static void main(String[] args) throws InterruptedException {
List<String> urls = new ArrayList<>();
urls.add("https://github.com/");
urls.add("https://stackoverflow.com/");
urls.add("https://www.zhihu.com/");
urls.add("https://www.reddit.com/");
urls.add("https://www.linkedin.com/");
CountDownLatch latch = new CountDownLatch(urls.size());
System.out.println("开始爬虫任务...");
for (String url : urls) {
new Thread(new Crawler(url, latch)).start();
}
latch.await();
System.out.println("所有爬虫任务都已完成!");
}
}
4.2 运行结果
开始爬虫任务...
爬取 https://www.zhihu.com/ 成功, 内容大小: 37783 字符
爬取 https://github.com/ 成功, 内容大小: 227576 字符
爬取 https://stackoverflow.com/ 成功, 内容大小: 171290 字符
爬取 https://www.linkedin.com/ 成功, 内容大小: 12603 字符
爬取 https://www.reddit.com/ 失败, 原因: Read timed out
所有爬虫任务都已完成!
五、稍复杂点的示例代码
5.1 代码示例讲解
这是一个稍复杂的 CountDownLatch
示例。
在这个例子中,我们将模拟一个简单的赛车游戏,
- 其中有一个倒计时开始。
- 一旦倒计时结束,赛车就开始比赛,
- 当所有赛车完成比赛时,主线程打印一条消息。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchAdvancedDemo {
public static void main(String[] args) throws InterruptedException {
int numberOfRacers = 5;
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch finishSignal = new CountDownLatch(numberOfRacers);
// 创建赛车线程
for (int i = 0; i < numberOfRacers; i++) {
new Thread(new Racer(startSignal, finishSignal)).start();
}
// 模拟倒计时
System.out.println("倒计时开始...");
for (int i = 3; i > 0; i--) {
System.out.println("倒计时: " + i);
TimeUnit.SECONDS.sleep(1);
}
System.out.println("比赛开始!");
startSignal.countDown(); // 启动信号
// 等待所有赛车完成比赛
finishSignal.await();
System.out.println("所有赛车都完成了比赛!");
}
static class Racer implements Runnable {
private CountDownLatch startSignal;
private CountDownLatch finishSignal;
public Racer(CountDownLatch startSignal, CountDownLatch finishSignal) {
this.startSignal = startSignal;
this.finishSignal = finishSignal;
}
@Override
public void run() {
try {
// 等待开始信号
startSignal.await();
// 正在比赛
System.out.println(Thread.currentThread().getName() + " 开始比赛...");
Thread.sleep((long) (Math.random() * 10000));
System.out.println(Thread.currentThread().getName() + " 完成比赛!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 完成比赛后,递减完成信号计数
finishSignal.countDown();
}
}
}
}
在这个例子中,我们创建了两个 CountDownLatch
,
- 一个用于开始信号 (
startSignal
), - 另一个用于完成信号 (
finishSignal
)。创建赛车线程时,它们都需要等待开始信号。
当倒计时结束时,调用 startSignal.countDown()
,开始信号变为0
,并表示比赛开始。
每个线程在模拟赛车完成比赛后,调用 finishSignal.countDown()
减少完成信号计数。
主线程使用 finishSignal.await()
等待所有赛车线程都完成比赛。当计数值变为 0 时,主线程将打印一条消息表示所有赛车都完成了比赛。
5.2 运行结果
倒计时开始...
倒计时: 3
倒计时: 2
倒计时: 1
比赛开始!
Thread-4 开始比赛...
Thread-2 开始比赛...
Thread-0 开始比赛...
Thread-1 开始比赛...
Thread-3 开始比赛...
Thread-4 完成比赛!
Thread-1 完成比赛!
Thread-0 完成比赛!
Thread-2 完成比赛!
Thread-3 完成比赛!
所有赛车都完成了比赛!