一、介绍

    CountDownLatch 是 Java 中的一个并发工具类,用于协调多个线程之间的同步。其作用是让某一个线程等待多个线程的操作完成之后再执行。它可以使一个或多个线程等待一组事件的发生,而其他的线程则可以触发这组事件。

二、特性

1. CountDownLatch 可以用于控制一个或多个线程等待多个任务完成后再执行。

2. CountDownLatch 的计数器只能够被减少,不能够被增加。

3. CountDownLatch 的计数器初始值为正整数,每次调用 countDown() 方法会将计数器减 1,计数器为 0 时,等待线程开始执行。

三、实现原理

    CountDownLatch 的实现原理比较简单,它主要依赖于 AQS(AbstractQueuedSynchronizer)框架来实现线程的同步。

    CountDownLatch 内部维护了一个计数器,该计数器初始值为 N,代表需要等待的线程数目,当一个线程完成了需要等待的任务后,就会调用 countDown() 方法将计数器减 1,当计数器的值为 0 时,等待的线程就会开始执行。

四、适用场景

1. 主线程等待多个子线程完成任务后再继续执行。例如:一个大型的任务需要被拆分成多个子任务并交由多个线程并行处理,等所有子任务都完成后再将处理结果进行合并。

2. 启动多个线程并发执行任务,等待所有线程执行完毕后进行结果汇总。例如:在一个并发请求量比较大的 Web 服务中,可以使用 CountDownLatch 控制多个线程同时处理请求,等待所有线程处理完毕后将结果进行汇总。

3. 线程 A 等待线程 B 执行完某个任务后再执行自己的任务。例如:在分布式系统中,一个节点需要等待其他节点的加入后才能执行某个任务,可以使用 CountDownLatch 控制节点的加入,等所有节点都加入完成后再执行任务。

4. 多个线程等待一个共享资源的初始化完成后再进行操作。例如:在某个资源初始化较慢的系统中,可以使用 CountDownLatch 控制多个线程等待共享资源初始化完成后再进行操作。

CountDownLatch 适用于多线程任务的协同处理场景,能够有效提升多线程任务的执行效率,同时也能够降低多线程任务的复杂度和出错率。

五、注意事项

1. CountDownLatch 对象的计数器只能减不能增,即一旦计数器为 0,就无法再重新设置为其他值,因此在使用时需要根据实际需要设置初始值。

2. CountDownLatch 的计数器是线程安全的,多个线程可以同时调用 countDown() 方法,而不会产生冲突。

3. 如果 CountDownLatch 的计数器已经为 0,再次调用 countDown() 方法也不会产生任何效果。

4. 如果在等待过程中,有线程发生异常或被中断,计数器的值可能不会减少到 0,因此在使用时需要根据实际情况进行异常处理。

5. CountDownLatch 可以与其他同步工具(如 Semaphore、CyclicBarrier)结合使用,实现更复杂的多线程同步。

六、实际应用

1. 案例一

    (1) 场景

    一个简单的 CountDownLatch 示例,演示了如何使用 CountDownLatch 实现多个线程的同步。

    (2) 代码

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * CountDownLatchCase1
 * 如何使用CountDownLatch实现多个线程的同步。
 *
 * @author wxy
 * @since 2023-04-18
 */
public class CountDownLatchCase1 {
    private static final Logger LOGGER = LoggerFactory.getLogger(CountDownLatchCase1.class);

    public static void main(String[] args) throws InterruptedException {
        // 创建 CountDownLatch 对象,需要等待 3 个线程完成任务
        CountDownLatch latch = new CountDownLatch(3);

        // 创建 3 个线程
        Worker worker1 = new Worker(latch, "worker1");
        Worker worker2 = new Worker(latch, "worker2");
        Worker worker3 = new Worker(latch, "worker3");

        // 启动 3 个线程
        worker1.start();
        worker2.start();
        worker3.start();

        // 等待 3 个线程完成任务
        latch.await();

        // 所有线程完成任务后,执行下面的代码
        LOGGER.info("All workers have finished their jobs!");
    }
}

class Worker extends Thread {
    private static final Logger LOGGER = LoggerFactory.getLogger(Worker.class);

    private final CountDownLatch latch;

    public String name;

    public Worker(CountDownLatch latch, String name) {
        this.latch = latch;
        this.name = name;
    }

    @Override
    public void run() {
        try {
            // 模拟任务耗时
            TimeUnit.MILLISECONDS.sleep(1000);
            LOGGER.info("{} has finished the job!", name);
        } catch (InterruptedException e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            // 一定要保证每个线程执行完毕或者异常后调用countDown()方法
            // 如果不调用会导致其他线程一直等待, 无法继续执行
            // 建议放在finally代码块中, 防止异常情况下未调用countDown()方法
            latch.countDown();
        }
    }
}

    运行结果:

CountDownLatch介绍和使用【Java多线程必备】_多线程

    在上面的代码中,首先创建了一个CountDownLatch对象,并指定需要等待的线程数为 3。然后创建了 3 个线程并启动。每个线程会模拟执行一个耗时的任务,执行完成后会调用 countDown() 方法将计数器减 1。在所有线程都完成任务后,主线程会执行 latch.await() 方法等待计数器为 0,然后输出所有线程都完成任务的提示信息。

    思考:如果不使用CountDownLatch情况将会是怎样呢?

    运行结果:

CountDownLatch介绍和使用【Java多线程必备】_多线程_02

    由执行结果可知,主线程不会等待子线程结束后再执行。如果我们主线程(main) 需要其他线程执行后的结果,我们就需要使用countDownLantch让主线程和执行快的线程等待子线程全部执行完毕再向下执行。

    思考:如果某个线程漏调用.countDown();会怎么样呢?

   接下来我们模拟worker1线程异常,如果该线程异常latch.countDown()方法就无法被调用。

public void run() {
    try {
        // 模拟任务耗时
        if ("worker1".equals(name)) {
            throw new RuntimeException(name + "运行异常");
        }
        TimeUnit.MILLISECONDS.sleep(1000);
        LOGGER.info("{} has finished the job!", name);
        latch.countDown();
    } catch (InterruptedException e) {
        LOGGER.error(e.getMessage(), e);
    }
}

    运行结果:

CountDownLatch介绍和使用【Java多线程必备】_CountDownLatch_03

    由运行结果可知,当worker1线程由于异常没有执行countDown()方法,最后state结果不为0,导致所有线程停在latch.await()方法内自旋(死循环)。所以程序无法结束。(如何解决这个问题呢?请看案例二)

CountDownLatch介绍和使用【Java多线程必备】_java_04

2. 案例二

    (1) 场景

    当年刚工作不久,遇到一个这样的问题:远程调用某个api,大部分情况下需要2-3s才能读取到响应值。我需要解析响应的JSON用于后续的操作。由于这个调用是异步的,我没办法在主线程获取到响应的JSON值。

    当时第一时间想到的是让主线程休眠,但是休眠多久好呢?1、2、3s?显然是不行的,如果1s就请求成功并响应了,你要等3s,这不是浪费时间吗!

    于是,我就请教了公司一位大佬。他告诉我使用CountDownLatch。我恍然大悟,之前自己学过,但是一到战场上我就把他给忘记了(实践是检验真理的唯一标准)。

(2) 代码(偷个懒 哈哈 就是在案例一的代码中修改了await()方法)

    将latch.await()修改为 latch.await(5, TimeUnit.SECONDS),这段代码啥意思呢?就是当第一个线程到达await()方法开始计时,5s后不等待未执行完毕的线程,直接向下执行。这么写的好处是,当调用某个方法超时太久,不影响我们的主逻辑。(很实用)

// 等待 3 个线程完成任务
if (!latch.await(5, TimeUnit.SECONDS)) {
    LOGGER.warn("{} time out", worker1.name);
}

// 所有线程完成任务后,执行下面的代码
LOGGER.info("all workers have finished their jobs!");

    看一下加了latch.await(5, TimeUnit.SECONDS)方法后执行结果:

CountDownLatch介绍和使用【Java多线程必备】_java_05