CountDownLatch 超详细完整版讲解(Java AQS 底层 + 源码 + 场景 + 踩坑 + 对比)

 

CountDownLatch 超详细完整版讲解(Java AQS 底层 + 源码 + 场景 + 踩坑 + 对比)

一、名词

正确类名:CountDownLatch 包路径:java.util.concurrent.CountDownLatch

二、核心定义

1. 本质

同步闭锁(Latch),基于 AQS(AbstractQueuedSynchronizer)共享模式实现的线程等待工具。 闭锁:一扇只能打开一次的门。

  • 初始化时设置一个计数器 state
  • 线程调用 countDown() → state = state - 1
  • 任意线程调用 await() 会阻塞,直到 state == 0,门打开,所有等待线程全部放行
  • 一旦 state 归 0,永久无法恢复,不能重复使用

2. 两大核心角色

  1. 等待线程:调用 await(),阻塞等待计数器归零(通常是主线程)
  2. 工作线程:执行业务,完成后调用 countDown() 扣减计数

三、完整 API 全解

1. 构造方法

public CountDownLatch(int count)

  

  • count:计数器初始值,代表需要等待完成的任务 / 线程数量
  • 限制:count 不能 < 0,否则直接抛出 IllegalArgumentException

2. countDown()

public void countDown()

  

执行逻辑:

  1. 内部调用 AQS 共享释放 releaseShared(1)
  2. 将同步状态 state - 1
  3. 如果减完后 state == 0:唤醒所有阻塞在 await() 的线程
  4. 如果 state 已经是 0,调用此方法无任何效果
  5. 不会抛出中断异常

3. await () 无超时阻塞

public void await() throws InterruptedException
  • 阻塞当前线程,直到 state=0 才返回
  • 如果等待途中线程被 interrupt(),直接抛出 InterruptedException,清除中断标记
  • 使用场景:确定所有任务一定会完成,不考虑超时

4. await (long timeout, TimeUnit unit) 带超时阻塞

public boolean await(long timeout, TimeUnit unit) throws InterruptedException

  

返回值规则:

  • true:在超时时间内 state 变为 0,全部任务完成
  • false:超时到了 state 仍大于 0,有任务未完成 参数:
  • timeout:时长数值
  • unit:时间单位 TimeUnit.SECONDS/MILLISECONDS 作用:防止死锁永久阻塞,生产环境必用

5. getCount()

public long getCount()

  

返回当前剩余未扣减的计数值,即当前 AQS 的 state 值。 注意:返回值是瞬时快照,多线程并发下读取的值不保证实时准确,仅用于日志 / 监控。

四、底层 AQS 源码拆解(精简核心逻辑)

CountDownLatch 内部静态内部类 Sync 继承 AQS:

private static final class Sync extends AbstractQueuedSynchronizer {
    // 初始化同步状态state = count
    Sync(int count) {
        setState(count);
    }

    // 获取当前剩余计数
    int getCount() {
        return getState();
    }

    // 共享式获取锁:await底层调用
    protected int tryAcquireShared(int acquires) {
        // state == 0 返回1(获取成功,不阻塞)
        // state > 0 返回-1(获取失败,进入阻塞队列)
        return (getState() == 0) ? 1 : -1;
    }

    // 共享式释放锁:countDown底层调用
    protected boolean tryReleaseShared(int releases) {
        // CAS自旋循环扣减state
        for (;;) {
            int c = getState();
            // 已经是0,无需处理
            if (c == 0)
                return false;
            int nextc = c - 1;
            // CAS更新state
            if (compareAndSetState(c, nextc))
                // 扣减后等于0,返回true,触发唤醒所有等待线程
                return nextc == 0;
        }
    }
}

  

底层流程总结

  1. await()tryAcquireShared()
    • state>0:线程进入 AQS 阻塞队列,挂起
  2. countDown()tryReleaseShared()
    • CAS 自旋 state-1
    • 若 state==0:唤醒队列里所有等待线程,全部恢复运行

五、两种经典业务实战案例

案例 1:主线程等待多子线程全部执行完成(批量查询)

关键点:countDown() 必须放在 finally,防止任务异常导致计数不扣减、主线程卡死

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class LatchBatchQueryDemo {
    public static void main(String[] args) throws InterruptedException {
        // 3个并行查询任务
        int taskSize = 3;
        CountDownLatch latch = new CountDownLatch(taskSize);

        for (int i = 1; i <= taskSize; i++) {
            int taskId = i;
            new Thread(() -> {
                try {
                    System.out.println("任务" + taskId + "开始查询数据");
                    // 模拟数据库/远程接口耗时
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println("任务" + taskId + "查询完成");
                } catch (InterruptedException e) {
                    System.out.println("任务" + taskId + "被中断");
                } finally {
                    // 无论正常/异常,都必须扣减计数
                    latch.countDown();
                    System.out.println("任务" + taskId + "扣减计数,剩余:" + latch.getCount());
                }
            }).start();
        }

        System.out.println("主线程等待所有查询任务结束...");
        // 最多等待5秒,避免永久阻塞
        boolean allFinish = latch.await(5, TimeUnit.SECONDS);
        if (allFinish) {
            System.out.println("全部查询完成,合并数据返回");
        } else {
            System.out.println("部分任务超时未完成,剩余计数:" + latch.getCount());
        }
    }
}

  

案例 2:压测场景 —— 所有线程同时并发执行(双 Latch 经典用法)

需求:10 个线程先全部就绪,收到统一信号后同时发起请求,模拟并发峰值

  • startLatch:放行门,初始 1,主线程 countDown 一次,所有工作线程同时启动
  • endLatch:结束门,初始 10,主线程等待所有线程执行完毕
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class LatchPressureTestDemo {
    public static void main(String[] args) throws InterruptedException {
        int threadNum = 10;
        // 启动闸门:控制所有线程同时开始
        CountDownLatch startLatch = new CountDownLatch(1);
        // 结束闸门:主线程等待全部线程跑完
        CountDownLatch endLatch = new CountDownLatch(threadNum);

        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "已就绪,等待统一执行信号");
                    // 全部阻塞在这里,等主线程开门
                    startLatch.await();

                    // 并发业务逻辑
                    System.out.println(Thread.currentThread().getName() + "发起请求");
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    endLatch.countDown();
                }
            }).start();
        }

        TimeUnit.SECONDS.sleep(1);
        System.out.println("=== 统一下发执行信号 ===");
        // 闸门打开,所有线程同时执行
        startLatch.countDown();

        // 等待所有压测线程结束
        endLatch.await();
        System.out.println("所有并发请求执行完毕");
    }
}

  

六、四大典型业务使用场景

  1. 多数据源并行查询 同时调用多个微服务、多库查询,全部拿到结果后再统一组装返回,缩短接口 RT。
  2. 系统启动初始化 Spring 服务启动时,多线程加载缓存、初始化连接池、拉取配置;主线程等待所有初始化完成再开启端口对外提供服务。
  3. 接口并发压测 双 Latch 实现精准同一时刻批量请求,消除线程逐个启动带来的时间差。
  4. 多任务分片处理 大文件分片、批量数据分片,分片线程全部处理完成后主线程做汇总、归档。

七、核心特性(重点区分同类工具)

  1. 计数器单向不可逆 countDown 只能递减,state 一旦到 0,无法重置。如果需要循环复用,必须使用 CyclicBarrier
  2. 等待线程无数量限制 可以 N 个线程同时调用 await() 阻塞,计数器归零后全部一起唤醒。
  3. 工作线程与等待线程完全解耦 CountDownLatch 不要求等待线程和工作线程一一对应,主线程单独等待一批子线程是标准用法。
  4. 不支持任务回调 计数器归 0 后没有内置执行回调方法,CyclicBarrier 支持构造传入屏障回调任务。

八、CountDownLatch vs CyclicBarrier vs Semaphore 完整对比表

 

对比维度CountDownLatchCyclicBarrierSemaphore
核心作用 一组线程完成后唤醒等待线程 一组线程互相等待全部到达屏障再放行 控制同一时间并发线程数量(限流)
计数器 单向递减,归零作废,不可复用 循环复用,每次凑齐指定数量自动重置 许可 acquire 减少、release 增加,可循环
等待主体 外部主线程等待工作线程 所有线程互相等待对方 线程抢许可,无等待分组逻辑
回调支持 无内置回调 支持屏障完成执行回调 Runnable 无回调
底层 AQS 模式 共享锁 独占锁 + 重入机制 共享锁
典型场景 批量任务汇总、启动等待、压测闸门 多阶段分段并行计算 接口限流、连接池控制

九、生产环境高频踩坑点 + 解决方案

坑 1:countDown () 未放入 finally,任务异常导致永久阻塞

错误写法:

// 错误:发生异常直接跳过countDown
try {
    doBiz();
    latch.countDown();
} catch (Exception e) {
    log.error("异常");
}

  

修复:countDown () 固定写在 finally 块,无论是否异常都扣减计数。

坑 2:初始化 count 和实际 countDown 调用次数不匹配

  • count=5,但只调用 3 次 countDown → state 永远 > 0,await 死锁
  • count=3,但调用 5 次 countDown:前 3 次归零,后 2 次无效,逻辑正常但浪费调用 解决:严格保证每个工作线程对应一次 countDown,线程数量 = count 初始值。

坑 3:生产环境直接使用无超时 await ()

服务器任务阻塞、线程卡死时,主线程永久挂起,耗尽线程池资源。 强制规范:业务代码一律使用 await(time, unit) 超时版本,增加兜底逻辑。

坑 4:多轮循环复用同一个 CountDownLatch

循环第二次时 state 已经是 0,await 直接放行,失去等待效果。 重复等待场景改用 CyclicBarrier。

坑 5:忽略 InterruptedException 不处理

线程中断后直接抛出异常,流程中断,建议捕获异常后主动执行 countDown 释放计数。

坑 6:用 getCount () 做业务判断

getCount 只是瞬时快照,并发下数据不准确,仅用于打印日志监控,不要作为业务分支判断条件。

十、扩展补充:CountDownLatch 与线程池搭配最佳实践

实际项目不会手动 new Thread,统一搭配 ThreadPoolExecutor

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

for (int i = 0; i < 5; i++) {
    pool.submit(() -> {
        try {
            // 业务逻辑
        } finally {
            latch.countDown();
        }
    });
}
latch.await(10, TimeUnit.SECONDS);
pool.shutdown();

  

十一、总结一句话记忆

CountDownLatch = 一次性单向门闩,主线程等待 N 个工作线程全部扣完计数后统一放行,适合一次性批量等待场景,不可循环复用;生产必须加超时、countDown 写在 finally 防止死锁。

 

 

微信图片_20260323111735_56_204

 

posted @ 2026-07-22 15:12  程序员食堂  阅读(9)  评论(0)    收藏  举报