欢迎关注我的公众号:观知小阁。包含各种类的文章,内容更丰富,更新及时且不迷路。

大家好!今天我们来聊聊Java并发编程中一个非常实用的工具 —— CountDownLatch。

如果你在面试中被问到“如何让主线程等待多个子线程执行完成”,或者在实际开发中遇到需要协调多个线程的场景,那么CountDownLatch绝对是你的得力助手!

1. 什么是CountDownLatch?

想象一下这样的场景:一辆旅游大巴要发车,司机必须等所有乘客都上车后才能启动。这里的“乘客数量”就是CountDownLatch的“计数阈值”,乘客上车一次计数减1,减到0,司机就能发车。

CountDownLatch是Java并发包(java.util.concurrent)中的一个同步辅助类,它允许一个或多个线程等待其他线程完成操作后再继续执行。你可以把它理解为一个倒计时门闩,当计数归零时,门闩打开,等待的线程得以继续执行。

2. 核心原理:基于AQS的巧妙设计

CountDownLatch的底层实现依赖于Java并发框架的核心 —— AQS(AbstractQueuedSynchronizer)。几乎所有JUC包中的并发工具(如ReentrantLock、Semaphore等)都基于AQS实现。

2.1 源码核心剖析

CountDownLatch的代码非常简洁,所有核心逻辑都封装在内部的Sync类中:

public class CountDownLatch {
    // 内部同步器,继承AQS
    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            setState(count); // AQS的state就是CountDownLatch的计数
        }
        
        // 核心方法1:判断是否能继续执行
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
        
        // 核心方法2:计数递减
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0) return false;
                int nextc = c - 1;
                if (compareAndSetState(c, nextc)) {
                    return nextc == 0; // 只有减到0时才返回true
                }
            }
        }
    }
}

工作原理简述:

  • 初始化:创建CountDownLatch时设置初始计数值,存入AQS的state变量

  • await():调用await()的线程会检查state是否为0,不为0则进入AQS等待队列被挂起

  • countDown():每个工作线程完成任务后调用countDown(),通过CAS原子操作将state减1

  • 唤醒:当state减到0时,唤醒所有在等待队列中的线程

3. 核心API与使用步骤

CountDownLatch的API非常简单,主要包含构造函数和三个方法:

  • CountDownLatch(int count):构造函数,设置初始计数值

  • await():使当前线程等待,直到计数器变为0

  • countDown():将计数器减1

  • await(long timeout, TimeUnit unit):带超时的等待方法

标准使用步骤:

// 1. 创建CountDownLatch对象,指定需要等待的任务数
CountDownLatch latch = new CountDownLatch(3);

// 2. 启动多个工作线程
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            // 执行具体任务
            doWork();
        } finally {
            // 3. 任务完成后调用countDown()
            latch.countDown();
        }
    }).start();
}

// 4. 主线程等待所有任务完成
latch.await();
System.out.println("所有任务已完成!");

4. 实战应用场景

4.1 场景1:主线程等待多个子任务完成

这是最经典的用法。比如解析Excel的多个sheet,每个sheet用一个线程处理,主线程需要等待所有sheet解析完成后统计总耗时。

public class ExcelParser {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        long startTime = System.currentTimeMillis();
        
        // 解析sheet1
        new Thread(() -> {
            parseSheet("sheet1", 2000);
            latch.countDown();
        }).start();
        
        // 解析sheet2  
        new Thread(() -> {
            parseSheet("sheet2", 5000);
            latch.countDown();
        }).start();
        
        latch.await();
        long endTime = System.currentTimeMillis();
        System.out.println("总耗时:" + (endTime - startTime) + "ms");
    }
}

4.2 场景2:模拟并发测试的“发令枪”

需要多个线程同时开始执行测试时,可以使用两个CountDownLatch:

// 发令枪:所有线程等待开始信号
CountDownLatch startSignal = new CountDownLatch(1);
// 完成信号:主线程等待所有线程完成
CountDownLatch doneSignal = new CountDownLatch(5);

for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            startSignal.await(); // 等待开始信号
            doTest();
        } finally {
            doneSignal.countDown();
        }
    }).start();
}

// 确保所有线程就绪后,同时开始
Thread.sleep(100);
startSignal.countDown(); // 鸣枪,所有线程同时开始
doneSignal.await(); // 等待所有线程完成

4.3 场景3:服务启动依赖管理

在微服务架构中,应用启动时需要等待多个服务初始化完成:

public class ServiceStarter {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        
        // 初始化数据库服务
        new Thread(() -> {
            initDatabase();
            latch.countDown();
        }).start();
        
        // 初始化缓存服务
        new Thread(() -> {
            initCache();
            latch.countDown();
        }).start();
        
        // 初始化消息队列
        new Thread(() -> {
            initMQ();
            latch.countDown();
        }).start();
        
        latch.await();
        System.out.println("所有服务初始化完成,应用启动成功!");
    }
}

5. CountDownLatch vs CyclicBarrier:如何选择?

很多同学容易混淆CountDownLatch和CyclicBarrier,这里做个清晰对比:

特性CountDownLatchCyclicBarrier
核心思想一个/多个线程等待其他线程完成多个线程互相等待,到达屏障点后一起继续
重用性一次性,计数归零后无法重置可循环使用,自动重置
适用模型主从模式(主线程等待工作线程)对等协作模式(所有线程地位平等)
额外功能无支持barrierAction(屏障触发时执行的任务)
异常处理简单复杂,某线程异常会导致屏障损坏

选择口诀:

  • 等别人干完活 → 用CountDownLatch

  • 大家齐步走 → 用CyclicBarrier

6. 最佳实践与避坑指南

6.1 务必在finally块中调用countDown()

try {
    doWork();
} finally {
    latch.countDown(); // 确保即使发生异常也能减少计数
}

6.2 使用带超时的await()避免永久阻塞

// 最多等待3秒
if (latch.await(3, TimeUnit.SECONDS)) {
    // 所有任务完成
} else {
    // 超时处理
    System.err.println("任务执行超时!");
}

6.3 合理设置初始计数值

初始计数值应与实际需要等待的任务数严格一致,避免因多减或少减导致程序逻辑错误。

6.4 不要滥用CountDownLatch

对于简单的线程等待,也可以使用Thread.join(),但CountDownLatch更加灵活,不要求等待的线程必须结束。

6.5 与线程池配合使用

在实际项目中,通常配合线程池使用CountDownLatch:

ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(10);

for (int i = 0; i < 10; i++) {
    executor.submit(() -> {
        try {
            doTask();
        } finally {
            latch.countDown();
        }
    });
}

latch.await();
executor.shutdown();

7. 性能优化建议

  • 避免频繁创建:CountDownLatch是一次性的,如果需要重复使用类似功能,考虑使用CyclicBarrier

  • 合理设置线程数:根据任务类型和系统资源设置合适的线程数

  • 监控等待时间:在生产环境中监控await()的等待时间,及时发现性能瓶颈

8. 总结

CountDownLatch是Java并发编程中不可或缺的工具,它通过简单的计数器模型,优雅地解决了“一个线程等待多个线程”的协作问题。其设计精巧、API简洁,是处理多线程同步的利器。

关键要点回顾:

  • CountDownLatch基于AQS实现,state变量存储计数值

  • await()使线程等待,countDown()减少计数

  • 计数归零时唤醒所有等待线程

  • 一次性使用,不可重置

  • 务必在finally中调用countDown()避免死锁

  • 与CyclicBarrier根据场景选择使用

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐