1. 引言

在 Java 多线程编程中,线程间的有效协调至关重要,这能确保同步正确性并防止数据损坏。CountDownLatch 和 Semaphore 是两种常用的线程协调机制。本文将深入探讨两者的核心差异,并分析各自的使用场景。

2. 基础概念

2.1 CountDownLatch

CountDownLatch 允许一个或多个线程优雅地暂停,直到指定的一组任务完成。它通过递减计数器工作,当计数器归零时表示所有前置任务已结束。

2.2 Semaphore

Semaphore 是通过许可证控制共享资源访问的同步工具。与 CountDownLatch 不同,Semaphore 的许可证可在整个应用中多次释放和获取,实现更精细的并发管理。

3. 核心差异对比

3.1 计数机制

  • CountDownLatch
    ✅ 从初始计数开始,任务完成时递减计数器
    ✅ 计数归零后释放所有等待线程

  • Semaphore
    ✅ 维护一组许可证,每个许可证代表一次资源访问权限
    ✅ 线程获取许可证访问资源,完成后释放许可证

3.2 可重置性

  • Semaphore
    ✅ 许可证可多次释放和获取,支持动态资源管理
    ⚠️ 示例:当应用需要更多数据库连接时,可动态释放额外许可证

  • CountDownLatch
    ❌ 计数归零后无法重置或复用
    ⚠️ 设计为一次性使用场景

3.3 动态许可证调整

  • Semaphore
    ✅ 运行时通过 acquire()release() 动态调整许可证数量
    ✅ 允许实时修改并发访问资源的线程数

  • CountDownLatch
    ❌ 初始化后计数固定,运行时无法修改

3.4 公平性

  • Semaphore
    ✅ 支持公平模式(FIFO),按请求顺序分配许可证
    ⚠️ 防止高竞争场景下的线程饥饿

  • CountDownLatch
    ❌ 无公平性概念
    ⚠️ 适用于线程执行顺序不敏感的一次性同步

3.5 典型使用场景

  • CountDownLatch 适用场景
    ✅ 协调多线程启动
    ✅ 等待并行操作完成
    ✅ 系统初始化同步(如数据处理前确保所有数据加载完成)

  • Semaphore 适用场景
    ✅ 管理共享资源访问
    ✅ 实现资源池(如数据库连接池)
    ✅ 控制关键代码段访问
    ✅ 限制并发数据库连接数

3.6 性能对比

  • CountDownLatch
    ✅ 仅涉及计数器递减,开销极低
    ⚠️ 适合高频同步场景

  • Semaphore
    ❌ 许可证管理引入额外开销
    ⚠️ 频繁调用 acquire()/release() 会影响性能
    ⚠️ 高并发场景下性能损耗更明显

3.7 差异总结表

特性 CountDownLatch Semaphore
核心目的 线程同步等待任务完成 控制共享资源访问
计数机制 递减计数器 管理许可证(令牌)
可重置性 ❌ 不可重置(一次性同步) ✅ 可重置(许可证可复用)
动态许可证调整 ❌ 不支持 ✅ 运行时动态调整
公平性 ❌ 无保证 ✅ 支持公平模式(FIFO)
性能开销 ✅ 低(最小化处理) ❌ 较高(许可证管理开销)

4. 实现对比

4.1 CountDownLatch 实现

int numberOfTasks = 3;
CountDownLatch latch = new CountDownLatch(numberOfTasks);

for (int i = 1; i <= numberOfTasks; i++) {
    new Thread(() -> {
        System.out.println("Task completed by Thread " + Thread.currentThread().getId());
        latch.countDown();
    }).start();
}

latch.await();
System.out.println("All tasks completed. Main thread proceeds.");

关键点
✅ 计数归零后调用 countDown() 无效
✅ 后续 await() 调用立即返回(不阻塞线程)

latch.countDown();
latch.await(); // 不会阻塞
System.out.println("Latch is already at zero and cannot be reset.");

输出示例

Task completed by Thread 11
Task completed by Thread 12
Task completed by Thread 13
All tasks completed. Main thread proceeds.
Latch is already at zero and cannot be reset.

4.2 Semaphore 实现

int NUM_PERMITS = 3;
Semaphore semaphore = new Semaphore(NUM_PERMITS);

for (int i = 1; i <= 5; i++) {
    new Thread(() -> {
        try {
            semaphore.acquire();
            System.out.println("Thread " + Thread.currentThread().getId() + " accessing resource.");
            Thread.sleep(2000); // 模拟资源使用
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }).start();
}

关键点
⚠️ acquire() 可能被中断,需处理 InterruptedException
✅ 许可证可动态重置

try {
    Thread.sleep(5000);
    semaphore.release(NUM_PERMITS); // 重置许可证数量
    System.out.println("Semaphore permits reset to initial count.");
} catch (InterruptedException e) {
    e.printStackTrace();
}

输出示例

Thread 11 accessing resource.
Thread 12 accessing resource.
Thread 13 accessing resource.
Thread 14 accessing resource.
Thread 15 accessing resource.
Semaphore permits reset to initial count.

5. 结论

CountDownLatch 和 Semaphore 各有优势:

  • CountDownLatch
    ✅ 适用于固定任务集完成后才允许线程继续的场景
    ✅ 适合一次性同步事件(如系统初始化)

  • Semaphore
    ✅ 通过限制并发访问线程数控制共享资源
    ✅ 提供更精细的并发管理能力

选择时需考虑:
❌ 避免在需要动态资源管理的场景使用 CountDownLatch
❌ 避免在一次性同步场景过度使用 Semaphore(增加不必要开销)

示例代码已上传至 GitHub


原始标题:CountDownLatch vs. Semaphore | Baeldung