JUC系列之《CountDownLatch:同步多线程的精准发令枪 》

简介: CountDownLatch是Java并发编程中用于线程协调的同步工具,通过计数器实现等待机制。主线程等待多个工作线程完成任务后再继续执行,适用于资源初始化、高并发模拟等场景,具有高效、灵活、线程安全的特点,是JUC包中实用的核心组件之一。
  • 引言
  • 一、为什么需要CountDownLatch?
  • 二、核心概念与工作原理
  • 三、使用方式与API详解
  • 四、经典应用场景
  • 五、底层原理简析
  • 六、总结与最佳实践
  • 互动环节

引言

在多线程编程中,我们经常遇到这样的需求:需要等待多个线程都完成任务后,主线程才能继续执行。比如,主线程启动多个工作线程去加载系统所需的各项资源(数据库连接、缓存数据、配置文件等),必须等所有资源都加载完毕,主线程才能宣布系统启动完成。

如果用简单的Thread.join()来实现,代码会变得臃肿且难以维护。而
java.util.concurrent.CountDownLatch
(倒计时门闩)正是JDK为我们提供的优雅解决方案。它就像赛跑时的发令枪,所有运动员(子线程)准备就绪,等待枪响(计数器归零)后同时出发;或者像终点线,等待所有运动员冲线后比赛才结束。


一、为什么需要CountDownLatch?

想象一下以下几个场景:

  1. 游戏大厅:一个主线程需要等待所有玩家(子线程)都加载完地图和资源后,游戏才能开始。
  2. 压力测试:需要同时启动成千上万个线程去访问一个服务,以模拟高并发场景。
  3. 数据汇总:启动多个线程计算数据的不同部分,主线程需要等待所有部分计算完成后,才能进行汇总。

在这些场景下,核心需求是:一个或多个线程需要等待其他一组线程完成操作

传统方法的弊端

  • 使用Thread.join():需要持有每个线程的引用,并且无法复用。
  • 使用while循环忙等待:浪费CPU资源,效率低下。

CountDownLatch的优势在于它提供了一种标准化的、高效的、可复用的线程等待机制。

二、核心概念与工作原理

核心思想CountDownLatch通过一个计数器(count) 来实现同步。这个计数器在初始化时被设定为一个正数(通常是需要等待的线程数量)。

两大核心操作

  1. 倒计时(Count Down):每个完成任务的工作线程调用countDown()方法,计数器会减1。
  2. 等待(Await):等待的线程(通常是主线程)调用await()方法。该方法会阻塞,直到计数器减到0,所有等待的线程会被释放,继续执行。

重要特性

  • 一次性:计数器的值不能被重置。一旦计数器为0,所有对await()的调用都会立即通过,无法再次使用。如果需要重置,请考虑使用CyclicBarrier
  • 不可逆:计数器只能减少,不能增加。

三、使用方式与API详解

1. 核心API

  • CountDownLatch(int count):构造函数,参数count为需要倒数的次数。
  • void await():调用此方法的线程会被挂起,它会等待直到count值为0才继续执行。等待线程调用。
  • boolean await(long timeout, TimeUnit unit):与await()类似,但增加了超时时间。如果在指定时间内count值还没变为0,也会不再等待。
  • void countDown():将count值减1。如果减1后count值为0,则唤醒所有等待的线程。工作线程调用。
  • long getCount():获取当前的计数器值。

2. 基础用法示例

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class BasicCountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 模拟有3个任务需要完成
        int workerCount = 3;
        // 1. 创建CountDownLatch,初始化计数器为3
        CountDownLatch latch = new CountDownLatch(workerCount);
        for (int i = 1; i <= workerCount; i++) {
            final int taskId = i;
            new Thread(() -> {
                try {
                    // 模拟任务执行耗时
                    System.out.println("任务" + taskId + "正在执行...");
                    Thread.sleep((long) (Math.random() * 2000));
                    System.out.println("任务" + taskId + "执行完毕!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 2. 每个任务完成后,调用countDown(),计数器减1
                    // 务必放在finally块中,确保无论任务成功与否都会执行
                    latch.countDown();
                }
            }).start();
        }
        System.out.println("主线程等待所有任务完成...");
        // 3. 主线程调用await(),阻塞等待,直到计数器变为0
        latch.await();
        // 4. 所有任务完成后,主线程继续执行
        System.out.println("所有任务均已完成,主线程开始进行汇总工作...");
    }
}

输出可能为

主线程等待所有任务完成...
任务1正在执行...
任务2正在执行...
任务3正在执行...
任务1执行完毕!
任务3执行完毕!
任务2执行完毕!
所有任务均已完成,主线程开始进行汇总工作...

3. 带超时的等待

在实际生产中,为了避免线程无限期等待(比如某个子线程死锁或执行时间过长),建议使用带超时的await

// 在主线程中
try {
    // 最多等待5秒
    boolean allDone = latch.await(5, TimeUnit.SECONDS);
    if (allDone) {
        System.out.println("所有任务在5秒内完成!");
    } else {
        System.out.println("警告:有任务超时未完成!当前已完成任务数: " + (workerCount - latch.getCount()));
        // 这里可以执行一些超时处理逻辑,比如取消剩余任务、记录日志等
    }
} catch (InterruptedException e) {
    e.printStackTrace();
}

四、经典应用场景

场景一:模拟高并发(同时开始)

让所有线程在同一时刻开始执行,模拟真正的并发压力测试。

public class ConcurrentStartDemo {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 100;
        CountDownLatch startSignal = new CountDownLatch(1); // 开始信号,初始为1
        CountDownLatch doneSignal = new CountDownLatch(threadCount); // 完成信号
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 所有线程启动后,都在此等待“开始信号”
                    startSignal.await(); // 等待主线程“发令”
                    
                    // 模拟真正要测试的业务逻辑
                    doBusinessLogic();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    doneSignal.countDown();
                }
            }).start();
        }
        System.out.println("所有线程准备就绪...");
        Thread.sleep(2000); // 模拟主线程做一些准备工作
        System.out.println("发令枪响!所有线程同时开始执行!");
        startSignal.countDown(); // 关键一步:将开始信号计数器减为0,释放所有等待的线程
        doneSignal.await(); // 等待所有线程执行完毕
        System.out.println("所有线程执行完成!");
    }
    private static void doBusinessLogic() {
        // ... 业务代码 ...
    }
}

场景二:等待所有资源初始化(全部结束)

这是最常用的场景,主线程等待所有辅助线程完成初始化工作。

public class ResourceInitializationDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3); // 3个初始化任务
        // 初始化缓存
        new Thread(() -> {
            initCache();
            latch.countDown();
        }).start();
        // 初始化数据库连接池
        new Thread(() -> {
            initDatabasePool();
            latch.countDown();
        }).start();
        // 加载配置文件
        new Thread(() -> {
            loadConfig();
            latch.countDown();
        }).start();
        System.out.println("主线程等待所有初始化任务完成...");
        latch.await(); // 阻塞,直到3个任务的countDown()都被调用
        System.out.println("所有资源初始化完毕,系统启动成功!");
        // ... 启动服务器等后续操作
    }
    private static void initCache() { /* ... */ }
    private static void initDatabasePool() { /* ... */ }
    private static void loadConfig() { /* ... */ }
}

五、底层原理简析

CountDownLatch的实现是基于AQS(
AbstractQueuedSynchronizer)
这个强大的同步器框架。

  • 同步状态(state):在AQS中,有一个volatileint state变量。在CountDownLatch中,这个state就代表了计数器的值
  • await()方法:当线程调用await()时,它会去检查state是否为0。如果不是0,当前线程就会被构造成一个Node节点,通过AQS的机制加入到等待队列中,并被挂起(通过LockSupport.park())。
  • countDown()方法:调用countDown()会使用CAS操作state减1。如果CAS成功后发现state变成了0,就会去唤醒等待队列中的所有线程(通过LockSupport.unpark())。

正是基于AQS,CountDownLatch才能高效、线程安全地管理计数器和线程的阻塞与唤醒。

六、总结与最佳实践

  1. 核心作用CountDownLatch是一个同步辅助工具,用于协调多个线程之间的执行节奏,允许一个或多个线程等待另一组线程完成操作。
  2. 主要方法await()用于等待,countDown()用于发出“完成一个”的信号。
  3. 一次性:计数器无法重置,用完即废。如果需要循环使用,应考虑CyclicBarrier
  4. 最佳实践
  5. 务必在finally中countDown:确保即使任务执行过程中抛出异常,计数器也能被减少,避免主线程永远等待。
  6. 使用带超时的await:防止因为个别线程问题导致整个程序卡死,增强系统鲁棒性。
  7. 明确角色:分清谁是“等待者”(调用await),谁是“工作者”(调用countDown)。
  8. ** vs 其他工具**:
  9. Thread.join()CountDownLatch更灵活,不需要持有线程对象引用,且可以用于等待事件(而不一定是线程结束)。
  10. CyclicBarrierCyclicBarrier是等所有线程都到达一个屏障点后才能继续,并且可以重置复用;而CountDownLatch是等一个事件发生N次(计数器减到0),且不能重置。

CountDownLatch是JUC包中最简单却最实用的同步工具之一。理解并熟练运用它,能让你在设计多线程程序时更加得心应手,轻松解决复杂的线程同步问题。

相关文章
|
1月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
132 2
|
1月前
|
Java 测试技术 API
【JUC】(1)带你重新认识进程与线程!!让你深层次了解线程运行的睡眠与打断!!
JUC是什么?你可以说它就是研究Java方面的并发过程。本篇是JUC专栏的第一章!带你了解并行与并发、线程与程序、线程的启动与休眠、打断和等待!全是干货!快快快!
381 2
|
1月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
144 2
|
6月前
|
存储 缓存 安全
JUC并发—11.线程池源码分析
本文主要介绍了线程池的优势和JUC提供的线程池、ThreadPoolExecutor和Excutors创建的线程池、如何设计一个线程池、ThreadPoolExecutor线程池的执行流程、ThreadPoolExecutor的源码分析、如何合理设置线程池参数 + 定制线程池。
JUC并发—11.线程池源码分析
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
1127 6
【Java学习】多线程&JUC万字超详解
|
Java C++
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
129 0
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
|
设计模式 Java 调度
JUC线程池: ScheduledThreadPoolExecutor详解
`ScheduledThreadPoolExecutor`是Java标准库提供的一个强大的定时任务调度工具,它让并发编程中的任务调度变得简单而可靠。这个类的设计兼顾了灵活性与功能性,使其成为实现复杂定时任务逻辑的理想选择。不过,使用时仍需留意任务的执行时间以及系统的实际响应能力,以避免潜在的调度问题影响应用程序的行为。
208 1
|
Java API 调度
JUC线程池: FutureTask详解
总而言之,FutureTask是Java并发编程中一个非常实用的类,它在异步任务执行及结果处理方面提供了优雅的解决方案。在实现细节方面可以搭配线程池的使用,以及与Callable接口的配合使用,来完成高效的并发任务执行和结果处理。
140 0

热门文章

最新文章