CountDownLatch详解以及用法示例

一、什么是CountDownLatch

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓。

CountDownLatch的作用也是如此,在构造CountDownLatch(int count):的时候需要传入一个整数count,在这个整数“倒数”到0之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。

总结来说,CountDownLatch是Java中一个多线程工具类,用于控制线程的顺序,可以让主线程等待其他线程完成任务之后再继续执行,或者让多个线程之间相互等待。

二、主要方法

  1. CountDownLatch(int count):构造方法,创建一个新的 CountDownLatch 实例,用给定的计数初始化。参数 count 表示线程需要等待的任务数量。
int numberOfTasks = 5;
CountDownLatch latch = new CountDownLatch(numberOfTasks);
  1. void await():使当前线程等待,直到计数器值变为0,除非线程被 interrupted。如果计数器的值已经为0,则此方法立即返回。在实际应用中,通常在主线程中调用此方法,等待其他子线程完成任务。(会使线程休眠,直到countDownLatch的值递减到0,才会重新就绪)
latch.await();
  1. boolean await(long timeout, TimeUnit unit):使当前线程等待,直到计数器值变为0,或者指定的等待时间已到,或者线程被 interrupted。如果计数器的值已经为0,则此方法立即返回。
  • 参数 timeout 是指定的等待时间,
  • 参数 unit 是 timeout 的单位(如秒、毫秒等)。

此方法返回一个布尔值,表示在等待时间内计数器是否变为0。

latch.await(5, TimeUnit.SECONDS);

这里需要注意的是,await()方法并没有规定只能有一个线程执行该方法,如果多个线程同时执行await()方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁。

  1. void countDown():递减计数器的值。如果计数器的结果为0, 则释放所有等待的线程。在实际应用中,通常在线程完成任务后调用此方法。
latch.countDown();

这里需要注意的是,countDown()方法并没有规定一个线程只能调用一次,当同一个线程调用多次countDown()方法时,每次都会使计数器减一;

  1. long getCount():获取当前计数的值。返回当前 CountDownLatch 实例内部的计数值。
long remainingCount = latch.getCount();

三、优缺点

  • 优点
  1. 简化了线程间的通信和同步。在某些并发场景中,需要等待其他线程完成任务后才能继续执行,使用 CountDownLatch 可以简化这种操作,而不需要复杂的锁和等待/通知机制。
  2. 提高性能。由于 CountDownLatch 可以让线程在完成任务后立即递减计数值,而不需要等待其他线程完成任务,因此可以减少阻塞,提高程序运行性能。
  3. 支持灵活的计数。可以通过创建不同的 CountDownLatch 实例,实现对多个线程任务计数。
  • 缺点:
  1. 单次使用。CountDownLatch 的计数值无法重置。一旦计数值到达零,它就不能再被使用了。在需要重复使用的场景中,可以选用 CyclicBarrier 或 Semaphore。
  2. 没有返回值。CountDownLatch 无法获得执行任务的线程所返回的结果。如果需要收集线程执行结果,可以考虑使用 java.util.concurrent.Future 和 java.util.concurrent.ExecutorService。

四、使用场景

  1. 启动多个线程执行并行任务,主线程等待所有并行任务完成后继续执行。
    例如:在测试中,准备数据阶段,需要同时查询多个子系统的数据和处理,等待处理结束后再进行下一步操作。
  2. 控制线程的执行顺序。一个线程需要等待其他线程的结果或者完成任务后才能继续执行。
    例如:一个文件解压缩程序,首先需要下载文件,下载完成后解压文件。
  3. 实现一个计数器,允许一个或多个线程等待直到计数器为0。这对于在系统初始化时,需要等待资源加载或者初始化的场景十分有用。
    例如:等待加载配置文件、启动连接池等操作完成后才开始处理其他任务。

五、示例代码

1、一个简单示例代码

在这个例子中,创建了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

2、启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行

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;
    }
    @Override
    public void run() {
        // 处理业务
        String name = Thread.currentThread().getName();
        System.out.println("子线程:" + name + "正在加载组件...");
        // 业务处理完毕,countDownLatch-1
        countDownLatch.countDown();
    }
}

结果:

等待子线程加载组件...
子线程:pool-1-thread-1正在加载组件...
子线程:pool-1-thread-2正在加载组件...
子线程:pool-1-thread-3正在加载组件...
子线程:pool-1-thread-4正在加载组件...
子线程:pool-1-thread-5正在加载组件...
所有组件加载完毕,关闭线程池pool...

3、示例

主线程定义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;
        }

        @Override
        public 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]号,到达终点...

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;
        }

        @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("所有爬虫任务都已完成!");
    }
}

运行结果

开始爬虫任务...
爬取 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、稍复杂点的示例代码

在这个例子中,我们将模拟一个简单的赛车游戏,

  • 其中有一个倒计时开始。
  • 一旦倒计时结束,赛车就开始比赛,
  • 当所有赛车完成比赛时,主线程打印一条消息。
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;
    }

    @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 时,主线程将打印一条消息表示所有赛车都完成了比赛。

运行结果:


倒计时开始...
倒计时: 3
倒计时: 2
倒计时: 1
比赛开始!
Thread-4 开始比赛...
Thread-2 开始比赛...
Thread-0 开始比赛...
Thread-1 开始比赛...
Thread-3 开始比赛...
Thread-4 完成比赛!
Thread-1 完成比赛!
Thread-0 完成比赛!
Thread-2 完成比赛!
Thread-3 完成比赛!
所有赛车都完成了比赛!

你可能感兴趣的:(Java,java)