考察点
这三个工具经常打包考,面试官想确认你不是只知道 API 名,而是分得清语义差异:谁是一次性的、谁可复用、谁控制并发度。能结合真实场景讲(大数据里太常见了:并行拉数、分片任务聚合、限流)会明显加分。追问常往「CountDownLatch 和 CyclicBarrier 区别」「Semaphore 能不能做连接池」「三个工具的底层实现」走。
参考答案
CountDownLatch:一次性倒计时门闩
语义:一个或多个线程等待另外 N 个事件完成。构造时指定计数,countDown() 减一,await() 阻塞到计数归零。
底层是 AQS 共享模式:state 初始化为计数 N,await 的线程排队等待 state 变 0;每次 countDown 把 state 减一,减到 0 时通过共享模式的传播机制,把队列里所有等待线程一次性唤醒。一次性是核心特征——state 到 0 后无法重置,再 await 直接返回。
典型场景:主线程等多个子任务完成。大数据里并行拉取最常用——一个任务要从 Hive、MySQL、HBase 三个数据源取数再合并,开三个线程并行拉,主线程用 CountDownLatch(3) 等齐。Spark 的 TaskSetManager 等待所有 task 结果也是类似思路。另一个用法是「发令枪」:计数设为 1,所有线程先 await 在门口,主线程一个 countDown,全部线程同时起跑,做并发压测时这么制造同时刻流量。
CyclicBarrier:可循环使用的栅栏
语义:一组线程互相等待,大家都到齐后再一起通过栅栏,执行下一步。.await() 的含义是「我到了,等其他人都到」。全部到达时可以触发一个可选的 barrierAction(由最后到达的线程执行)。
和 CountDownLatch 的关键区别有三:一是方向不同,CountDownLatch 是某些线程等事件,CyclicBarrier 是线程之间互相等;二是可复用,CyclicBarrier 到齐后自动重置进入下一代(generation),可以循环使用,Cyclic 名字由此而来;三是底层实现不同,它不是基于 AQS,而是 ReentrantLock + Condition,每代用 condition 的 await/signalAll 完成等待和放行。
还要注意 broken 语义:等待过程中某个线程被中断或超时,栅栏被打破(BrokenBarrierException),这一代其他等待线程全部收到异常——因为缺一个人大家永远等不齐,继续等就是死锁。用线程池配合 CyclicBarrier 时,线程数少于 parties 数会直接死等,这个坑很常见。
典型场景:多线程分片计算后汇总。比如把 1 亿条数据切 10 片,10 个线程各自统计,到齐后由 barrierAction 合并结果;如果还要再来一轮迭代计算(比如并行版的迭代算法),重置后直接进入下一代,这正是它比 CountDownLatch 合适的地方。
Semaphore:信号量,控制并发度
语义:维护一组许可,acquire 拿许可(没有就阻塞),release 还许可。作用是限制同时访问某资源的线程数,和锁的区别在于它允许多个线程同时进入。
底层同样是 AQS 共享模式,state 就是剩余许可数。构造时可以指定公平或非公平:非公平下新来线程会直接尝试抢刚释放的许可,吞吐高;公平模式严格按排队顺序发许可。tryAcquire 带超时参数,拿不到可以快速失败,比阻塞式更可控。
典型场景:
- 资源池:数据库连接池的逻辑本质——10 个连接就是 new Semaphore(10)。实际工程直接用 HikariCP,但手写简易连接池就是 Semaphore + 队列。
- 限流:调用第三方接口限 QPS 为 50,用 Semaphore(50) 配合许可归还,防止把下游打垮。
- 背压控制:生产者消费者场景,用 Semaphore 控制队列逻辑容量,满则生产者 acquire 阻塞,天然反压。大数据管道里 Local模式调试时这么做过简易限流。
注意一个细节:release 可以由任何线程调用,不要求必须由 acquire 的线程归还——这点和锁的所有权语义不同,既灵活也容易出错,用的时候要在文档或代码注释里约定清楚。
一句话选型
等别人干完活 → CountDownLatch(一次性);大家互相等齐再一起走 → CyclicBarrier(可复用);控制同时进入的数量 → Semaphore。三个工具覆盖「协同完成、协同对齐、协同限量」三种并发协作模式,背后都是 AQS 家族,读懂 AQS 就都通了。
可能的追问
- CountDownLatch 想复用怎么办? 不能复用,到 0 即终态。需要反复等待就换 CyclicBarrier,或者每轮新建一个 latch。JDK 7 的 Phaser 是更通用的替代品,支持动态注册参与方和多阶段。
- CyclicBarrier 的 barrierAction 在哪个线程执行? 最后到达栅栏的那个线程执行,执行完才放行所有等待线程。所以 barrierAction 别太重,否则拖慢整个放行。
- Semaphore 的 acquire 不可中断吗? acquire() 响应中断(抛 InterruptedException),acquireUninterruptibly() 不响应。还有 tryAcquire 可以带超时,生产上推荐用带超时的版本避免无限等待。
- 用 Semaphore 实现互斥锁可以吗? new Semaphore(1) 就是互斥信号量,但它没有重入语义、没有所有者概念(谁都能 release),和 ReentrantLock 不等价。特殊场景(跨线程释放,比如一个线程拿锁另一个线程还)反而只有它能做。