一、CountdownLatch是什么
CountdownLatch(闭锁/倒计时锁),是Java中用于多线程协作的工具类,核心功能是让一个或多个线程等待其他线程完成操作。
- 初始化计数器:new CountDownLatch(int count)进行初始化,count参代表需要等待完成任务的数量。
- 线程阻塞:当一个线程调用await()方法时,如果计数器的值不为 0,该线程会被阻塞,进入等待状态,直到计数器变为 0 或者线程被中断。
- 计数器递减:其他线程在完成自己的任务后,可以调用**countDown()**方法,该方法会将计数器的值减 1。这个操作是线程安全的,多个线程同时调用countDown()也能正确处理。
- 唤醒机制:当计数器的值通过countDown()方法递减到 0 时,所有因调用**await()**方法而被阻塞的线程会被唤醒,继续执行后续的代码逻辑。
二:快速上手:代码示例:
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
// 创建CountDownLatch,设置计数器初始值为3
CountDownLatch latch = new CountDownLatch(3);
// 创建并启动3个子线程
for (int i = 0; i < 3; i++) {
new Thread(new Worker(latch)).start();
}
try {
System.out.println("主线程等待子线程完成...");
// 主线程调用await()方法,进入阻塞状态
latch.await();
System.out.println("所有子线程已完成,主线程继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Worker implements Runnable {
private final CountDownLatch latch;
public Worker(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 开始工作");
// 模拟子线程工作
Thread.sleep((long) (Math.random() * 2000));
System.out.println(Thread.currentThread().getName() + " 工作完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 子线程工作完成,调用countDown()方法
latch.countDown();
}
}
}
控制台输出:
三、CountDownLatch的使用场景
CountDownLatch典型用法:
场景 说明 生活类比
多任务结果汇总 主线程等待所有子任务完成后汇总数据 组装电脑:等CPU、内存、硬盘安装完成再开机
模拟高并发测试 同时释放N个线程模拟用户并发请求 赛跑:所有选手等待发令枪统一出发
服务依赖启动检查 确保所有依赖服务就绪后主服务启动 火箭发射:检查燃料、导航、通信系统后点火
批量任务进度控制 游戏加载:等待地图、角色、音效资源加载完毕 电影开场:所有观众到齐后放映
场景 | 说明 | 生活类比 |
---|---|---|
多任务结果汇总 | 主线程等待所有子任务完成后汇总数据 | 组装电脑:等CPU、内存、硬盘安装完成再开机 |
模拟高并发测试 | 同时释放N个线程模拟用户并发请求 | 赛跑:所有选手等待发令枪统一出发 |
服务依赖启动检查 | 确保所有依赖服务就绪后主服务启动 | 火箭发射:检查燃料、导航、通信系统后点火 |
批量任务进度控制 | 游戏加载:等待地图、角色、音效资源加载完毕 | 电影开场:所有观众到齐后放映 |
四、CountDownLatch的不足
CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
使用注意事项
1.计数器的合理设置:在使用CountDownLatch时,计数器的初始值应根据实际任务需求合理设置。
2.countDown的位置:应尽量放在finally块中,以确保无论任务执行过程中是否发生异常,计数器都会被正确递减。若
3.在调用await()方法时,建议使用带超时参数的**await(long timeout, TimeUnit unit)**方法,以防止线程因某些异常情况导致永久阻塞。
五、底层原理:AQS熟悉爱你
CountDownLatch 的底层是基于 AbstractQueuedSynchronizer(AQS)实现的,AQS 是 Java 并发包中实现同步器的基础框架,它提供了一种高效的方式来管理同步状态和线程等待队列。
1. 关键组件
AQS 同步队列:这是一个双向链表,用于管理阻塞的线程。当一个线程调用await()方法且计数器不为 0 时,该线程会被封装成一个Node节点加入到同步队列中。每个Node包含了线程的引用、等待状态以及前驱和后继节点的引用。
volatile state
:AQS 中的state变量是一个volatile修饰的整数,用于记录当前计数器的值。volatile关键字保证了多线程环境下对state的读写操作具有可见性,即一个线程对state的修改能立即被其他线程看到。
CAS 原子操作:在更新state时,使用了 CAS(Compare and Swap)原子操作。CAS 操作包含三个操作数:内存地址、预期原值和新值。只有当内存地址中的实际值与预期原值相等时,才会将新值赋给内存地址,否则操作失败。这种原子操作保证了在多线程环境下对state的更新是线程安全的,避免了竞态条件。
2. 执行流程解析
await () 方法:当一个线程调用await()方法时,首先会检查state是否为 0。如果state不为 0,说明还有任务未完成,当前线程需要等待。此时,线程会被封装成一个Node节点加入到 AQS 同步队列的尾部,并将线程挂起。线程会进入一个自旋等待的过程,不断检查前驱节点是否为头节点且是否能成功获取共享资源(即state是否为 0)。如果条件满足,线程被唤醒并继续执行。
countDown () 方法:当一个线程调用countDown()方法时,会通过 CAS 操作将state减 1。如果减 1 后state的值变为 0,说明所有任务都已完成,此时会唤醒 AQS 同步队列中的所有等待线程。具体过程是从队列头部开始,依次唤醒每个等待的线程,让它们有机会重新竞争资源并继续执行。
内存泄漏风险
当使用CountDownLatch与ExecutorService结合时,要注意及时释放ExecutorService资源。如果ExecutorService未正确关闭,其中的线程可能会一直持有对CountDownLatch的引用,导致CountDownLatch无法被垃圾回收,从而产生内存泄漏。在任务完成后,应及时调用ExecutorService的shutdown()或shutdownNow()方法关闭线程池,释放资源。
中断响应
在等待过程中,若线程被中断,应优先处理InterruptedException异常。CountDownLatch的await()方法会抛出InterruptedException,当捕获到该异常时,应根据业务逻辑进行适当处理,如记录日志、中断其他相关线程或进行资源清理等。避免在捕获异常后简单地忽略或继续执行,导致程序出现不一致的状态或其他潜在问题。
六、常见面试题
与CyclicBarrier区别?
- CountDownLatch:一次性使用,主线程等待子线程。
- CyclicBarrier:可重复使用,子线程相互等待。
state变量为什么用volatile修饰?
- 保证多线程间可见性,确保计数变化实时生效。
CAS操作有什么作用?
- 原子性更新计数器,避免多线程竞争导致计数错误。
七、总结
- 核心价值:简化多线程协作,实现"
等待-唤醒
"控制。 - 适用场景:任务协调、并发测试、服务依赖管理。
- 使用口诀:初始化计数 → 任务完成减1 → 归零唤醒主线程。