一、什么是CountDownLatch
CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓。
CountDownLatch的作用也是如此,在构造CountDownLatch(int count):的时候需要传入一个整数count,在这个整数“倒数”到0之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。
总结来说,CountDownLatch是Java中一个多线程工具类,用于控制线程的顺序,可以让主线程等待其他线程完成任务之后再继续执行,或者让多个线程之间相互等待。
二、主要方法
- CountDownLatch(int count):构造方法,创建一个新的 CountDownLatch 实例,用给定的计数初始化。参数 count 表示线程需要等待的任务数量。
int numberOfTasks = 5;
CountDownLatch latch = new CountDownLatch(numberOfTasks);
- void await():使当前线程等待,直到计数器值变为0,除非线程被 interrupted。如果计数器的值已经为0,则此方法立即返回。在实际应用中,通常在主线程中调用此方法,等待其他子线程完成任务。(会使线程休眠,直到countDownLatch的值递减到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。这对于在系统初始化时,需要等待资源加载或者初始化的场景十分有用。
例如:等待加载配置文件、启动连接池等操作完成后才开始处理其他任务。
五、示例代码
一个简单示例代码
在这个例子中,创建了5个线程,并让每个线程睡眠1秒钟,表示完成一个任务。在每个线程完成任务后,调用了countDown()方法,计数器减1。
在主线程中,调用了await()方法,等待所有线程完成任务。当所有线程的计数器都减为0时,主线程才会继续执行,输出"All tasks done"。
import java.util.concurrent.CountDownLatch;public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {int n = 5; // 等待5个线程完成任务CountDownLatch countDownLatch = new CountDownLatch(n);for (int i = 0; i < n; i++) {Thread t = new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + " is working");Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + " done");countDownLatch.countDown(); // 计数器减1} catch (InterruptedException e) {e.printStackTrace();}});t.start();}countDownLatch.await(); // 等待其他线程完成任务System.out.println("All tasks done");}
}
输出结果:
Thread-0 is working
Thread-1 is working
Thread-2 is working
Thread-3 is working
Thread-4 is working
Thread-2 done
Thread-1 done
Thread-0 done
Thread-4 done
Thread-3 done
All tasks done
启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行
package base.threadabout.multhread.countdownlatch;import java.util.concurrent.*;/*** CountDownLatch常用方法:await(),await(long,TimeUnit),countDown()* await()会使线程休眠,直到countDownLatch的值递减到0,才会重新就绪* await(long, TimeUnit) 休眠,直到countDownLatch的值递减到0或休眠时间结束* 大概作用:等所有线程&某些线程都执行完了,再统一执行某个具体功能*/
public class MainLoadingService {public static void main(String[] args) throws InterruptedException {CountDownLatch cdl = new CountDownLatch(5);ExecutorService pool = Executors.newFixedThreadPool(5);for (int i = 0; i < 5; i++) {Loading runnable = new Loading(cdl);pool.execute(runnable);}// 线程全部跑完的标志System.out.println("等待子线程加载组件...");cdl.await();System.out.println("所有组件加载完毕,继续执行...");pool.shutdown();}
}class Loading implements Runnable {private CountDownLatch countDownLatch;public Loading(CountDownLatch countDownLatch) {this.countDownLatch = countDownLatch;}@Overridepublic void run() {// 处理业务String name = Thread.currentThread().getName();System.out.println("子线程:" + name + "正在加载组件...");// 业务处理完毕,countDownLatch-1countDownLatch.countDown();}
}
结果:
等待子线程加载组件...
子线程:pool-1-thread-1正在加载组件...
子线程:pool-1-thread-2正在加载组件...
子线程:pool-1-thread-3正在加载组件...
子线程:pool-1-thread-4正在加载组件...
子线程:pool-1-thread-5正在加载组件...
所有组件加载完毕,关闭线程池pool...
示例
主线程定义new CountDownLatch(1)。每个子线程先执行await(),进入等待。等待所有子线程都开启,主线程执行countDown(),能确保所有子线程同时开始处理任务。
类似于赛跑,子线程是运动员,await是运动员的预备阶段,主线程是裁判,countDown是裁判的发令枪。枪响运动员才能跑。
package base.threadabout.multhread.countdownlatch;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class RaceGame {public static void main(String[] args) throws InterruptedException {ExecutorService pool = Executors.newFixedThreadPool(5);CountDownLatch countDownLatch = new CountDownLatch(1);for (int i = 0; i < 5; i++) {Player player = new Player(i, countDownLatch);pool.execute(player);}Thread.sleep(1000);System.out.println("所有选手各就位.....GO!");countDownLatch.countDown();pool.shutdown();}static class Player implements Runnable{private int id;private CountDownLatch countDownLatch;public Player(int id, CountDownLatch countDownLatch) {this.id = id;this.countDownLatch = countDownLatch;}@Overridepublic void run() {System.out.println("参赛选手[" + id +"]号,准备就绪...");try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("参赛选手[" + id +"]号,到达终点...");}}
}
结果:
参赛选手[0]号,准备就绪...
参赛选手[1]号,准备就绪...
参赛选手[2]号,准备就绪...
参赛选手[3]号,准备就绪...
参赛选手[4]号,准备就绪...
所有选手各就位.....GO!
参赛选手[1]号,到达终点...
参赛选手[3]号,到达终点...
参赛选手[0]号,到达终点...
参赛选手[2]号,到达终点...
参赛选手[4]号,到达终点...
示例代码
下面的示例展示了一个简单的网站爬虫,它使用 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;}@Overridepublic 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("所有爬虫任务都已完成!");}
}
运行结果
开始爬虫任务...
爬取 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
所有爬虫任务都已完成!
稍复杂点的示例代码
在这个例子中,我们将模拟一个简单的赛车游戏,
- 其中有一个倒计时开始。
- 一旦倒计时结束,赛车就开始比赛,
- 当所有赛车完成比赛时,主线程打印一条消息。
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++) {//这里虽然start,但是由于前面new了startSignal,并且实现类中await的影响会等待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;}@Overridepublic 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 时,主线程将打印一条消息表示所有赛车都完成了比赛。
运行结果:
倒计时开始...
倒计时: 3
倒计时: 2
倒计时: 1
比赛开始!
Thread-4 开始比赛...
Thread-2 开始比赛...
Thread-0 开始比赛...
Thread-1 开始比赛...
Thread-3 开始比赛...
Thread-4 完成比赛!
Thread-1 完成比赛!
Thread-0 完成比赛!
Thread-2 完成比赛!
Thread-3 完成比赛!
所有赛车都完成了比赛!