基本概念
CountDownLatch
基于一个计数器实现,在创建 CountDownLatch
对象时需要指定一个初始计数,这个计数代表需要等待完成的操作数量。线程可以调用 CountDownLatch
的 countDown()
方法将计数器减 1,而其他线程可以调用 await()
方法进入等待状态,直到计数器的值变为 0 才会继续执行。
使用场景
- 并行任务同步:当有多个并行任务需要完成,并且主线程需要等待所有并行任务都完成后才能继续执行后续操作时,可以使用
CountDownLatch
。例如,在多线程下载文件的场景中,主线程需要等待所有子线程都下载完成后再进行文件的合并操作。 - 资源初始化:在某些情况下,需要确保所有必要的资源都初始化完成后,主线程才能继续执行后续业务逻辑。可以使用
CountDownLatch
来协调资源初始化线程和主线程的执行顺序。
工作原理
- 初始化:创建
CountDownLatch
对象时,需要传入一个整数作为计数器的初始值。 - 计数递减:每个需要完成的任务线程在完成任务后调用
countDown()
方法,将计数器的值减 1。 - 等待操作:调用
await()
方法的线程会被阻塞,直到计数器的值变为 0。计数器变为 0 后,所有等待的线程会被释放,继续执行后续代码。
代码示例
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个 CountDownLatch 对象,初始计数为 3
CountDownLatch latch = new CountDownLatch(3);
// 创建并启动 3 个工作线程
for (int i = 0; i < 3; i++) {
final int taskId = i;
Thread worker = new Thread(() -> {
try {
System.out.println("线程 " + taskId + " 开始执行任务");
// 模拟任务执行
Thread.sleep((long) (Math.random() * 1000));
System.out.println("线程 " + taskId + " 完成任务");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 任务完成,计数器减 1
latch.countDown();
}
});
worker.start();
}
// 主线程等待所有工作线程完成任务
System.out.println("主线程等待所有任务完成...");
latch.await();
System.out.println("所有任务已完成,主线程继续执行");
}
}
代码解释
- 创建
CountDownLatch
对象:CountDownLatch latch = new CountDownLatch(3);
表示需要等待 3 个任务完成。 - 启动工作线程:通过
for
循环创建并启动 3 个工作线程,每个线程模拟执行一个任务,任务完成后调用latch.countDown()
方法将计数器减 1。 - 主线程等待:主线程调用
latch.await()
方法进入等待状态,直到计数器的值变为 0。 - 继续执行:当计数器的值变为 0 时,主线程被唤醒,继续执行后续代码。
注意事项
CountDownLatch
的计数器一旦初始化为某个值,就不能再重置。如果需要重置计数器,可以考虑使用CyclicBarrier
。await()
方法有两种形式:无参的await()
会一直等待,直到计数器变为 0;带参数的await(long timeout, TimeUnit unit)
会在指定的时间内等待,如果超过指定时间计数器仍未变为 0,线程会继续执行。
这种表述大体上抓住了 CountDownLatch
的核心功能,但存在一些细节需要更精准地说明,下面详细解释。
准确理解关键方法的作用
await()
方法
await()
方法会让调用它的线程(不一定是主线程,任何线程都可以调用)进入等待状态。当线程调用 await()
时,它会检查 CountDownLatch
内部计数器的值。如果计数器不为 0,线程就会被阻塞,进入等待状态;只有当计数器的值变为 0 时,线程才会从 await()
方法返回,继续执行后续代码。
countDown()
方法
每调用一次 countDown()
方法,CountDownLatch
的计数器就会减 1。不过,并不是每次调用 countDown()
都会唤醒其他线程。只有当计数器的值从大于 0 减为 0 时,才会唤醒所有在 await()
方法中等待的线程。
结合示例代码说明
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 创建一个 CountDownLatch 实例,初始计数器值为 3
CountDownLatch latch = new CountDownLatch(3);
// 启动 3 个工作线程
for (int i = 0; i < 3; i++) {
final int workerId = i;
new Thread(() -> {
try {
System.out.println("工作线程 " + workerId + " 开始工作");
// 模拟工作耗时
Thread.sleep(1000);
System.out.println("工作线程 " + workerId + " 完成工作");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 工作完成,计数器减 1
latch.countDown();
}
}).start();
}
System.out.println("主线程等待所有工作线程完成工作...");
// 主线程调用 await() 进入等待状态
latch.await();
System.out.println("所有工作线程已完成工作,主线程继续执行");
}
}
代码执行流程分析
- 初始化:创建
CountDownLatch
对象,将计数器初始值设为 3。 - 启动工作线程:启动 3 个工作线程,每个线程模拟执行一些工作,完成后调用
countDown()
方法。 - 主线程等待:主线程调用
await()
方法,此时计数器值为 3,主线程进入等待状态。 - 工作线程完成任务:每个工作线程完成任务后调用
countDown()
,计数器依次减 1。前两次调用countDown()
时,计数器不为 0,不会唤醒等待线程;当第三次调用countDown()
使计数器变为 0 时,会唤醒在await()
方法中等待的主线程。 - 主线程继续执行:主线程从
await()
方法返回,继续执行后续代码。
综上所述,await()
方法让调用线程等待计数器变为 0,countDown()
方法用于减少计数器的值,仅当计数器变为 0 时才会唤醒等待线程。
简易版实现
public class MyCountDownLatch {
private Integer count;
public MyCountDownLatch(int count){
this.count = count;
}
public synchronized void countDown(){
count--;
if (count == 0){
notify();//唤醒等待队列中的一个线程
//notifyAll();//唤醒等待队列中的所有线程
}
}
public synchronized void await() throws InterruptedException{
System.out.println(count);
if (count != 0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}