CountDownLatch 实现精准的并发控制
概述
本文档详细分析并发启动场景(赛跑模式)中两个 CountDownLatch 的作用和阻塞关系。
代码示例
importjava.util.concurrent.CountDownLatch;publicclassRaceStartDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{finalintrunnerCount=10;finalCountDownLatchreadyLatch=newCountDownLatch(runnerCount);// 计数器=10finalCountDownLatchstartLatch=newCountDownLatch(1);// 计数器=1// 启动10个运动员线程for(inti=0;i<runnerCount;i++){finalintid=i;newThread(()->{try{System.out.println("运动员 "+id+" 准备就绪");readyLatch.countDown();// ← 不阻塞,只是计数-1startLatch.await();// ← 阻塞的是运动员线程!System.out.println("运动员 "+id+" 起跑!");}catch(InterruptedExceptione){e.printStackTrace();}}).start();}// 主线程(裁判)readyLatch.await();// ← 阻塞的是主线程!System.out.println("\n所有运动员就位,准备发令!");Thread.sleep(500);startLatch.countDown();// ← 主线程执行,唤醒所有运动员System.out.println("砰!");}}两个 Latch 对比
| Latch | 谁调用 await() | 阻塞的是谁 | 谁调用 countDown() | 初始计数 | 作用 |
|---|---|---|---|---|---|
| readyLatch | 主线程 | 主线程 | 各个 Runner 线程 | 10 | 主线程等待所有运动员就位 |
| startLatch | Runner 线程 | Runner 线程 | 主线程 | 1 | 运动员等待发令枪 |
执行流程图
┌─────────────────────────────────────────────────────────────┐ │ Phase 1: 准备阶段 │ ├─────────────────────────────────────────────────────────────┤ │ Runner-1: readyLatch.countDown() (不阻塞) readyLatch: 10→9 │ │ Runner-2: readyLatch.countDown() (不阻塞) readyLatch: 9→8 │ │ ... │ │ Runner-10: readyLatch.countDown() (不阻塞) readyLatch: 1→0 │ │ │ │ 主线程: readyLatch.await() ←─────── 阻塞主线程! │ │ 等待 readyLatch 从 10 减到 0 │ └─────────────────────────────────────────────────────────────┘ ↓ readyLatch == 0 主线程被唤醒 ┌─────────────────────────────────────────────────────────────┐ │ Phase 2: 起跑阶段 │ ├─────────────────────────────────────────────────────────────┤ │ Runner-1: startLatch.await() ←─────── 阻塞运动员线程! │ │ Runner-2: startLatch.await() ←─────── 阻塞运动员线程! │ │ ... │ │ Runner-10: startLatch.await() ←────── 阻塞运动员线程! │ │ │ │ 主线程: startLatch.countDown() startLatch: 1→0 │ │ (发令枪!) │ └─────────────────────────────────────────────────────────────┘ ↓ startLatch == 0 所有运动员线程被唤醒,开始跑!形象类比:赛跑场场景
🏁 赛跑场场景 🏃♂️ Runner-1 🏃♂️ Runner-2 🏃♂️ Runner-10 │ │ │ │准备就绪 │准备就绪 │准备就绪 ▼ ▼ ▼ countDown() countDown() countDown() (报到) (报到) (报到) 👨💼 裁判(主线程) │ readyLatch.await() │ ⏳ 等待中... (所有人都报到了,readyLatch == 0) │ startLatch.countDown() ←─── 砰!发令枪 ────→ │ 所有 Runner 被唤醒,开始跑!详细时间线
时间轴 → T0: 主线程启动 10 个 Runner 线程 T1: Runner-1 执行 readyLatch.countDown() (readyLatch: 10→9) T2: Runner-2 执行 readyLatch.countDown() (readyLatch: 9→8) ... T10: Runner-10 执行 readyLatch.countDown() (readyLatch: 1→0) T11: 主线程的 readyLatch.await() 返回!主线程继续执行 T12: 主线程打印 "所有运动员就位,准备发令!" T13: 主线程 sleep 500ms T14: 主线程执行 startLatch.countDown() (startLatch: 1→0) T15: 所有阻塞在 startLatch.await() 的 Runner 线程被唤醒! T16: 所有 Runner 开始跑关键点总结
❌ 常见误解
错误理解:两个 Latch 阻塞的都是主线程
✅ 正确理解
| Latch | 阻塞的线程 | 谁来唤醒 |
|---|---|---|
| readyLatch | 主线程 | Runner 线程们(通过 countDown) |
| startLatch | Runner 线程们 | 主线程(通过 countDown) |
为什么需要两个 Latch?
只用一个 CountDownLatch 无法实现这种精确的同步:
- 只用 readyLatch:主线程可以等待运动员就位,但无法控制运动员同时起跑
- 只用 startLatch:运动员可以等待发令枪,但主线程不知道运动员是否都准备好了
双 Latch 设计实现了:
- 确保所有运动员都就位后,裁判才发令
- 确保所有运动员同时收到发令信号,公平起跑
适用场景
这种双 Latch 模式适用于:
- ✅ 需要多个线程同时启动的场景
- ✅ 需要主线程确认所有子线程准备就绪
- ✅ 需要精确控制并发开始时机
- ✅ 批量任务并行执行,要求同时开始
与 CyclicBarrier 的对比
| 特性 | CountDownLatch(双 Latch) | CyclicBarrier |
|---|---|---|
| 等待方向 | 互为依赖(互相等待) | 所有线程互相等待 |
| 重用性 | 不可重用 | 可循环使用 |
| 线程数量 | 主线程 + 子线程 | 所有线程地位平等 |
| 典型场景 | 主控并发 | 并行计算分阶段 |
总结
核心要点
- readyLatch 阻塞主线程:主线程等待所有子线程准备就位
- startLatch 阻塞子线程:子线程等待主线程发令
- 互相等待:实现精确的并发控制
- 一次性使用:CountDownLatch 计数归零后无法重置
阻塞关系速记
┌─────────────┐ ┌─────────────┐ │ 主线程 │────────▶│ readyLatch │ 阻塞主线程 │ (裁判) │ └─────────────┘ └─────────────┘ ▲ │ │ Runner 线程们 │ countDown() │ ┌─────────────┐ ┌─────────────┐ │ Runner们 │◀────────│ startLatch │ 阻塞 Runner 们 │ (运动员) │ └─────────────┘ └─────────────┘ ▲ │ │ 主线程 │ countDown()(发令枪)总结:readyLatch 让裁判等待运动员,startLatch 让运动员等待裁判。