尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CountDownLatch与CyclicBarrier深度解析:从源码到实战踩坑指南

CountDownLatch与CyclicBarrier深度解析:从源码到实战踩坑指南 最近辅导过不少准备跳槽的朋友发现一个很有意思的现象CyclicBarrier和CountDownLatch这道题几乎成了Java多线程面试的“钉子户”。十个人里八个人能背出“CountDownLatch是一次性的CyclicBarrier可以循环使用”但你再追问一句“如果在同一个线程池里提交5个任务用CyclicBarrier等这5个任务齐了再继续会不会出问题” 一半人就卡住了。这恰恰说明很多人背的是概念不是机制。这篇我打算从源码和真实使用场景两条线来拆这两个类不空谈面试话术重点讲清楚它们各自解决的并发协作问题、内部实现细节以及我在项目里实际踩过的坑。里面涉及到的代码示例是我在本地运行验证过的排查过程也按真实经历写你可以直接照着复现。不管你是准备面试还是项目里真要写多线程协作逻辑这篇应该能帮你省不少时间。1. 面试现场重现同一个问题两种截然不同的答法先回到面试场景。面试官问“CyclicBarrier和CountDownLatch有何不同”很多人的第一反应是“CountDownLatch不能复用CyclicBarrier可以复用。”这个答案对吗对但只对了一半。面试官真正想听的是你能不能从线程协作模型的角度去解释差异而不是简单罗列API特性。1.1 两种典型的应对方式我见过两类应聘者的回答对比非常鲜明第一类背书型。“CountDownLatch是计数器await()阻塞countDown()减一减到0就放行。CyclicBarrier是栅栏N个线程互相等都到了才一起执行。区别是CountDownLatch不可重用CyclicBarrier可以。”说完就没了。第二类理解型。“CountDownLatch解决的是一个或多个线程等待其他线程完成事件的问题。它的核心是计数器调用countDown()的线程本质上是在发送‘我干完了’的信号而调用await()的线程在等这个信号累计到位。它不关心谁在等也不要求等的人数和干活的人数一致更像一个门闩门开了所有等着的人都能过但门不会自动重新锁上。CyclicBarrier的语义完全不同。它要求一组线程之间互相等待每个线程到屏障点之后调用await()这个调用会阻塞直到所有参与方都到达然后一起释放。它内部维护的不只是计数器还有‘代(Generation)’的概念可以自动重置所以叫Cyclic。它和CountDownLatch最大的区别在于前者是‘等事件’后者是‘等同伴’。”高下立判。第二类回答把两个类的本质语义讲清楚了而且提到了CountDownLatch的“不要求等的人数和干活人数一致”这个关键点面试官马上就能判断你是真用过还是在背题。1.2 为什么这道题能区分出水平这道题之所以成为高频面试题是因为并发工具类的使用最能反映一个人对多线程协作模型的理解深度。很多人在实际项目中写并发代码只会new Thread join或者无脑用Future.get()根本不会根据业务场景选择合适的并发协作原语。而CountDownLatch和CyclicBarrier恰好代表了两种最典型、也最容易混淆的协作模型。背API表格的人面试官多问一句“CountDownLatch能不能用CyclicBarrier替代”就露馅了。能真正讲清楚这两个类区别的人通常对AQSAbstractQueuedSynchronizer也有一定了解因为CountDownLatch的底层实现就依赖AQS这属于加分项后面我会展开讲。2. CountDownLatch一次性的倒计时阀门它真正擅长什么CountDownLatch是java.util.concurrent包里一个非常轻量级的同步工具官方文档对它的描述是“允许一个或多个线程等待直到在其他线程中执行的一组操作完成”。听起来很抽象其实它的工作方式特别像一个倒计时计时器。2.1 核心语义与源码视角先看它最常用的两个方法// 计数器减1 public void countDown() // 阻塞等待直到计数器变为0 public void await() throws InterruptedException创建时必须指定计数初始值CountDownLatch latch new CountDownLatch(5);这5代表什么意思代表有5个“事件”需要完成。注意我的措辞事件不是线程。这非常关键。从源码看CountDownLatch内部有一个内部类Sync它继承自AbstractQueuedSynchronizerprivate static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID 4982264981922014374L; Sync(int count) { setState(count); } int getCount() { return getState(); } protected int tryAcquireShared(int acquires) { return (getState() 0) ? 1 : -1; } protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c getState(); if (c 0) return false; int nextc c - 1; if (compareAndSetState(c, nextc)) return nextc 0; } } }看到没有计数器就是AQS的state变量。countDown()相当于releaseShared(1)await()相当于acquireSharedInterruptibly(1)——当state不为0时线程进入AQS的等待队列当最后一个countDown()把state减到0时tryReleaseShared返回true唤醒所有等待线程。这个实现细节解释了CountDownLatch的两个重要特性第一计数机制基于事件而非线程。同一个线程可以多次调用countDown()比如一个线程处理完10个任务每次任务完成都countDown()一次也可以多个线程各自countDown()一次。await()的线程数量、调用次数跟计数初始值完全无关。这意味着CountDownLatch特别适合“N个任务完成后汇总”的场景任务的归属方和数量由业务自由控制。第二state一旦变成0就不会再变回去所以它天然是一次性的。这是设计使然不是缺陷。如果你需要重复使用没问题那就是CyclicBarrier的领域了。2.2 典型使用场景并行任务合并我最常用的一个场景是并行请求合并。比如一个接口需要同时调用用户服务、订单服务、库存服务三个结果全部拿到再组装返回。用CountDownLatch可以写得很干净public UserOrderDetail queryDetail(String userId) throws InterruptedException { CountDownLatch latch new CountDownLatch(3); // 使用线程池执行三个并行任务 UserInfo userInfo new UserInfo(); ListOrder orders new ArrayList(); StockInfo stockInfo new StockInfo(); executor.execute(() - { try { userInfo userService.getUser(userId); } finally { latch.countDown(); } }); executor.execute(() - { try { orders orderService.getOrders(userId); } finally { latch.countDown(); } }); executor.execute(() - { try { stockInfo stockService.getStockInfo(userId); } finally { latch.countDown(); } }); latch.await(3, TimeUnit.SECONDS); // 最多等3秒避免接口挂死 return assemble(userInfo, orders, stockInfo); }这里有几个细节值得说一下countDown()必须放在finally里。如果任务抛异常计数器没减到0await()会一直阻塞。这个错误我在线上遇到太多次了后面排查章节我会单独讲。await()一定要用带超时的重载。latch.await(3, TimeUnit.SECONDS)而不是latch.await()。否则某个服务异常导致countDown永远凑不齐整个线程就挂死了线程池很快被耗尽这是线上事故的常见原因。线程池里的核心线程数要大于等于并行任务数否则任务排队等latch超时了任务还没执行完结果就是接口频繁超时。别以为latch只是等任务它等不到你的时候任务可能还在队列里。CountDownLatch还有一个经典用法同时启动多个线程。比如要压测某个操作希望所有线程在同一时刻开始。用一个初始值为1的CountDownLatch所有工作线程先await()主线程准备好后调用countDown()所有工作线程同时被释放。这个场景await()的线程数量没有限制latch的计数只为一次“起跑信号”服务非常契合它的“事件信号”语义。3. CyclicBarrier核心机制与“循环”到底怎么理解CyclicBarrier直译过来是“循环栅栏”名字有点拗口但语义特别好理解一组线程互相等待直到所有线程都到达一个公共屏障点然后一起继续执行。就像一群人约好出去旅游得等人齐了才发车。3.1 核心语义与源码视角CyclicBarrier的构造方法有两个public CyclicBarrier(int parties) public CyclicBarrier(int parties, Runnable barrierAction)parties是参与方的数量barrierAction是当所有线程到达屏障点时优先执行的一个动作。这个动作由最后到达的那个线程执行。它的核心方法只有一个public int await() throws InterruptedException, BrokenBarrierException每个参与线程执行到屏障点后就调用await()然后阻塞。当最后一个线程调用await()时屏障被触发所有线程被唤醒一起继续往下执行。有人会问“这不跟CountDownLatch差不多吗都是计数都是凑够了放行。” 表面上确实像但底层机制有本质区别。CyclicBarrier内部没有用AQS而是基于ReentrantLock和Condition实现的public class CyclicBarrier { private final ReentrantLock lock new ReentrantLock(); private final Condition trip lock.newCondition(); private final int parties; private final Runnable barrierCommand; private Generation generation new Generation(); private int count; // ... }关键在这里每个参与线程await()时count减1如果count不为0就进入Condition队列等待当count变为0时执行barrierCommand如果有然后nextGeneration()唤醒所有等待线程并重置count为partiesgeneration更换为新的一代。private void nextGeneration() { // signal completion of last generation trip.signalAll(); // set up next generation count parties; generation new Generation(); }这就是“Cyclic”的由来每次屏障被触发后计数器会自动重置可以进入下一轮等待。它内置了“代generation”的概念来管理这种循环生命周期。3.2 “循环”不是简单的重新计数很多面试者把“循环”理解成“用完可以再new一个”这就完全理解偏了。CyclicBarrier的“循环”是对象自身状态的自动重置不是让你创建新对象。举个例子一个数据批处理系统每批处理1000条数据分4个线程并行处理每批数据都要求4个线程先同步一下再开始下一批。如果用CyclicBarrier这个逻辑可以写得很自然public class BatchProcessor { private static final int THREAD_COUNT 4; private final CyclicBarrier barrier; private final ExecutorService executor; public BatchProcessor() { this.barrier new CyclicBarrier(THREAD_COUNT, () - { System.out.println(所有线程已同步开始下一批处理); }); this.executor Executors.newFixedThreadPool(THREAD_COUNT); } public void process(ListListInteger batches) throws InterruptedException { for (int batchIndex 0; batchIndex batches.size(); batchIndex) { ListInteger batch batches.get(batchIndex); for (int i 0; i THREAD_COUNT; i) { int threadId i; executor.execute(() - { try { // 模拟每个线程处理自己负责的那部分数据 ListInteger subBatch batch.subList(threadId * 250, (threadId 1) * 250); processSubBatch(subBatch); barrier.await(); // 等待其他线程完成本批处理 } catch (InterruptedException | BrokenBarrierException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } }); } } } }这里每个线程处理完自己负责的250条数据就调用barrier.await()等4个线程都处理完本批数据barrier触发一次下一批开始。4个线程反复使用同一个barrier对象完成多批数据的同步等待这就是“循环”的实际意义。注意这里的barrier是外部循环每批数据都用同一个实例而不是每批new一个。这样写的好处是当所有线程都到达屏障点后CyclicBarrier自动重置无需额外代码。3.3 barrierAction的特殊角色第二个构造方法里的Runnable参数barrierAction是很多面试官喜欢追问的点。它会在屏障触发时执行而且只执行一次由最后一个到达的线程执行。这个设计适合什么场景比如每批数据处理完成后需要合并所有线程的处理结果或者做日志记录、统计汇总。由于只执行一次天然避免了多个线程重复执行合并逻辑的问题。我之前在一个分片下载工具里用到过每个分片一个线程所有分片下载完成后由一个线程做文件合并。当时用CountDownLatch也能做但需要额外维护一个“是否已合并”的布尔标记配合synchronized或者AtomicBoolean写起来很啰嗦。换成CyclicBarrier的barrierAction合并逻辑直接放到回调里代码简洁很多也不容易出并发bug。4. 两者核心差异从面试官角度逐个追问这一节可以说是全文精华我按照面试官可能追问的角度把差异点逐个拆开讲。你如果能把这几个角度都答到位这道面试题基本就稳了。4.1 语义模型等事件 vs 等同伴这是最根本的差异。CountDownLatch的语义是“等事件完成”。调用await()的线程在等某个计数归零的事件它不关心是谁执行的countDown()也不要求这些事件之间有任何关联。比如主线程等3个异步任务完成这3个任务之间没有任何依赖关系主线程也不参与任务执行。CyclicBarrier的语义是“等同伴到达”。每个参与线程都在等其他参与线程所有线程都必须到达同一个屏障点谁也不能先走。它强调的是线程间的相互等待每个等待者既是等待方也是被等待方。一个比较形象的理解CountDownLatch像是下课铃——同学们等铃响铃响后各自去吃饭但铃不会自己重新挂上去CyclicBarrier像是旅行团集合——导游和团员互相等人齐了出发到了下一个景点再集合人齐了再出发可以反复进行。4.2 复用性一次性 vs 循环使用CountDownLatch的计数器归零后不能重置如果你需要再等一轮只能重新new一个CountDownLatch对象。这是一次性设计的底层AQS的state一旦从N减到0就不会再变回来。CyclicBarrier每次屏障触发后自动重置计数器同一个对象可以继续使用。如果你希望屏障触发后不自动重置而是永久打开那CyclicBarrier反而不合适得用CountDownLatch或者结合其他手段。这个差异直接决定了它们适用的业务场景。一次性等待用CountDownLatch循环同步用CyclicBarrier这是基本判断。4.3 等待者的角色与数量要求CountDownLatch对等待者数量没有限制await()可以被任意多个线程调用。初始计数N只表示需要countDown() N次不代表必须有N个线程来await。甚至可以是同一个线程countDown() N次。CyclicBarrier则要求参与线程数量固定必须与构造时指定的parties一致。所有participant线程都要调用await()而且调用await()的线程数量不能多不能少——多了会有线程一直阻塞到下一轮少了则永远无法触发屏障。这个差异在实际使用中会导致一个常见问题CyclicBarrier的parties数量和线程池线程数不匹配时代码会挂死或行为异常。比如parties设置4但线程池只有3个线程那最后一个线程永远等不到一直阻塞。这个问题我后面详细讲。4.4 API差异countDown/await vs awaitCountDownLatch的方法分成两类角色countDown()由“干活”的线程调用await()由“等待”的线程调用。这两个角色是分离的一个线程可以只countDown()不await()也可以只await()不countDown()。CyclicBarrier只有一个await()方法每个参与线程都扮演同一个角色到达屏障点并等待同伴。没有像countDown()那样的“独立通知”方法。这个API设计的差异也体现在使用习惯上CountDownLatch更灵活CyclicBarrier更规矩。它俩实现的不同分工模型决定了你在写代码时脑子里的思路完全不同。4.5 异常处理BrokenBarrierException独占这是CyclicBarrier特有的坑也是面试时的高频追问点。CountDownLatch的await()会抛出InterruptedException但不涉及“屏障损坏”的概念。CyclicBarrier的await()除了InterruptedException外还会抛出BrokenBarrierException。这个异常表示屏障已经被破坏导致依赖它的所有线程都无法继续正常等待。什么情况会破坏屏障一个等待线程在await()期间被中断InterruptedException其他所有等待线程都会收到BrokenBarrierException。一个等待线程await()超时比如使用了await(long timeout, TimeUnit unit)屏障进入broken状态。内部barrierAction执行抛出异常屏障也会进入broken状态。屏障一旦broken后续所有await()调用都会直接抛出BrokenBarrierException不会继续等待。这一点非常关键CyclicBarrier不像CountDownLatch那么“钝感”它对异常非常敏感一个线程出问题所有人都要跟着处理异常。我在一个实际项目里就遇到过这个情况。当时用CyclicBarrier做多线程分片计算其中一个线程处理超时await()加了超时参数超时后BrokenBarrierException瞬间让所有其他线程的计算全部失败。当时为了快速解决把同步机制换成了CountDownLatch——因为CountDownLatch容忍“部分线程失败”其他线程的结果还能用。这个经历让我深刻理解了“异常模型决定适用范围”这句话。4.6 内部实现AQS vs ReentrantLockConditionCountDownLatch基于AQS的共享锁实现线程进入等待队列后被park最后一个countDown()通过releaseShared唤醒所有等待线程。CyclicBarrier基于ReentrantLock和Condition实现每个等待线程调用await()时实际上执行的是condition.await()最后一个线程触发barrier时执行condition.signalAll()唤醒所有等待线程同时重置状态。这两种实现路径在性能上差别不大但反映了一个设计思想CountDownLatch借鉴的是“共享锁释放时唤醒所有等锁线程”的模型CyclicBarrier借鉴的是“条件队列广播通知”的模型。面试时如果能提到这一层说明你看过源码会加分不少。下面用一张表把核心差异汇总一下对比维度CountDownLatchCyclicBarrier核心语义一个或多个线程等待一组事件完成一组线程互相等待到达屏障点后一起放行复用性一次性计数归零后不可重置可循环使用自动重置等待方等待者与countDown()方相互独立数量不限所有参与方都必须调用await()数量固定关键APIcountDown() / await()await()异常类型InterruptedExceptionInterruptedException BrokenBarrierException底层实现AQS共享锁ReentrantLock Condition回调动作无支持barrierAction由最后到达线程执行适用的业务模型任务并行执行后汇总多轮分批同步推进5. 实际项目里最容易踩的坑与完整排查链路光会答面试题不够项目里真用起来坑比想象中多。这一节我结合自己的实战经历把最典型的几个问题和排查思路完整写出来希望你能跳过这些坑。5.1 坑一countDown()没执行线程池被活活耗死这是CountDownLatch最常见的线上事故。现象某服务接口偶发超时过一会儿线程池线程全部阻塞服务不可用重启后恢复但过段时间又重演。排查过程第一步先看线程栈。用jstack dump线程快照发现大量线程阻塞在CountDownLatch.await()上pool-3-thread-12 waiting on java.util.concurrent.CountDownLatch$Sync at java.util.concurrent.CountDownLatch.await(Native Method)第二步定位到具体代码。发现有一个异步任务在执行业务逻辑时抛出了异常但countDown()写在了业务代码之后异常导致countDown()根本不会执行。第三步复现验证。写了一个最小demo模拟其中一个子任务抛出RuntimeException主线程await()确实一直卡死。确认根因countDown()没有放在finally块里。这个问题的本质是异常中断了countDown()的调用链而await()还在傻等。修复方式很简单executor.execute(() - { try { doSomething(); } catch (Exception e) { log.error(task failed, e); // 记录失败状态供后续判断 } finally { latch.countDown(); } });后来我把团队里的并发代码规范加了一条凡是CountDownLatch.countDown()一律放finally没有任何例外。这个规范看起来严格但能避免一类非常隐蔽的线程池耗尽事故。5.2 坑二CyclicBarrier配合线程池parties和线程数不一致导致永久阻塞现象某个批处理程序配置了4个线程并行处理但运行一段时间后batch处理卡住不往下走了。日志里没有异常就是没结果。排查过程第一步看线程栈发现4个工作线程都在CyclicBarrier.await()上等待worker-1 waiting on java.util.concurrent.CyclicBarrier at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207) at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)第二步看配置CyclicBarrier初始化的时候parties4线程池核心线程数也是4。表面上没问题。第三步深挖发现问题出在线程池队列。当线程池中的4个核心线程都在await()等待时如果第5个任务被提交它是无法进入线程池执行的——因为核心线程已经全被占用了。而第5个任务恰恰是打破僵局需要的“第四个人”比如一个任务内部又会拆分成子任务子任务也要经过同一个barrier。第四步复盘整个流程主流程拆了4个task但其中某个task内部又通过同一个线程池异步调用了另一个需要经过barrier的task。线程池只有4个线程4个线程都在第一层await()第二层的task根本得不到执行第一层永远等不齐。死锁形成。这个问题非常隐蔽因为它不是典型的“parties 线程数”直接暴露而是在任务嵌套提交时线程池饱和导致CyclicBarrier永远凑不齐。解决方案方案A确保所有调用CyclicBarrier.await()的线程都是独立、不依赖线程池内其他任务完成的线程。方案Bparties设置的值必须等于同时能够到达屏障点的最大线程数注意别把嵌套子任务算进去。方案CCyclicBarrier.await()一定要带超时参数配合超时后的异常处理避免永久阻塞。虽然不能解决问题但至少能让故障快速暴露而不是默默挂死。barrier.await(5, TimeUnit.SECONDS);超时后会抛出TimeoutException屏障进入broken状态源码会唤醒其他等待线程并抛出BrokenBarrierException。此时所有线程都能感知到异常从而结束阻塞执行兜底逻辑或者记录错误退出。5.3 坑三CyclicBarrier屏障损坏后线程集体“雪崩”现象一个多线程分片计算程序某个线程计算超时后其他线程也“莫名其妙”报错退出。日志里全是BrokenBarrierException。排查过程第一步看日志发现第一个异常是TimeoutException后续全是BrokenBarrierException。两种异常时间点几乎一致。第二步分析原因。线程A调用barrier.await(3, TimeUnit.SECONDS)超时抛出TimeoutException同时CyclicBarrier把这个“代”标记为broken。其他正在await()的线程B、C、D立刻收到BrokenBarrierException从阻塞中退出。第三步理解“雪崩”的本质CyclicBarrier是一个强一致的同步点一个线程出问题会导致所有参与线程的同步状态失效。这不是CyclicBarrier的bug是设计行为。但这种行为在某些业务场景下可能不是预期的特别是当某些线程只是临时超时其他线程结果仍然有效的时候。应对策略如果业务不能接受“一损俱损”有两个思路思路一改用CountDownLatch实现“等待一定数量的完成事件”但允许未完成的事件不参与最终结果。这种弱一致的处理逻辑更贴近部分成功的业务场景。思路二捕获BrokenBarrierException后根据业务需要决定是否重试。重试时要注意CyclicBarrier已经broken可能需要reset()重置或者干脆new一个新的CyclicBarrier。我之前做支付对账模块时一个批次的对账分多个线程查渠道账单要求所有渠道结果都拿到才能做差异比对。某个渠道频繁超时导致整个批次失败重跑。后来我把同步方式换成了CountDownLatch 超时超时的渠道按“无差异”跳过或者打标记人工介入整个批次的成功率立刻上来了。这让我养成一个习惯涉及外部依赖的并发任务先想清楚“部分失败”能不能接受如果能尽量不要用CyclicBarrier。5.4 坑四线程复用的“假等待”还有一个容易忽略的问题发生在使用线程池配合CyclicBarrier时。场景线程池有4个线程CyclicBarrier的parties也是4但任务不是“每批固定4个”而是“来了一个任务就提交一次每个任务内部await()”。第一批4个任务到达屏障点正常触发。第二批又来了4个任务也正常。看起来没问题。但如果某一批只来了3个任务第4个任务迟迟不来呢那3个线程就会一直阻塞在await()上线程池里的线程被占满后续任务无法执行。这种“线程饥饿导致的假等待”最可怕的地方在于日志里没有任何异常看起来只是“慢”实际上已经死锁了。解决思路CyclicBarrier使用场景一定要配合超时参数这是保命手段。然后把“每批任务数量固定”做成硬约束从入口保证parties一定能凑齐。6. 面试追问拆解从入门到进阶的答题路线最后给准备面试的朋友梳理一条答题路线从简单到深入每一层都有对应的关键点你可以按自己的知识储备选择答到什么深度。6.1 第一层基础概念先准确说出两个类各自的作用CountDownLatch一个或多个线程等待其他线程完成一系列操作计数器归零后放行不可复用。CyclicBarrier一组线程互相等待都到达屏障点后一起放行可循环复用。这里注意措辞“一个或多个线程等待其他线程完成操作”是CountDownLatch的语义“一组线程互相等待”是CyclicBarrier的语义。别把“等待其他线程”和“互相等待”说混了。6.2 第二层对比差异按下面这个顺序讲语义不同等事件 vs 等同伴。复用性不同一次性 vs 循环。等待方数量CountDownLatch不限CyclicBarrier必须固定。角色分工CountDownLatch的countDown()和await()分离CyclicBarrier所有线程都是同一个await()。异常CyclicBarrier有BrokenBarrierExceptionCountDownLatch没有。这五个点说完基本能证明你是理解型的不是背题型的。6.3 第三层结合场景面试官如果继续追问“你实际在哪里用过”你最好能结合自己的项目讲不要只背概念。可以参考以下话术“我上一个项目里有个并行查询接口要同时调用户、订单、库存三个服务我用CountDownLatch做聚合等待初始值设3三个异步任务各countDown一次主线程await带超时。选它的原因是因为这是一个一次性的等待场景三个任务跑完就结束不需要复用而且CountDownLatch对等待线程数没有限制主线程一个await就够。另一个批处理场景数据分片后多线程分别计算每算完一轮需要同步结果再进入下一轮这种循环同步我用的是CyclicBarrierparties设成线程池核心线程数barrierAction里做汇总。选它是因为需要反复使用同一个同步点CountDownLatch没法自动重置。”这样回答既有深度又有实践面试官基本不可能再追问“你是不是只是背了面试题”这种问题。6.4 第四层源码与原理如果面试官继续深挖可以主动提到以下源码细节CountDownLatch内部有Sync类继承AQS计数是statecountDown()对应releaseSharedawait()对应acquireSharedInterruptibly。CyclicBarrier基于ReentrantLock和Condition有Generation内部类管理“代”每次触发后nextGeneration()重置count并signalAll()。CyclicBarrier的barrierCommand由最后一个到达的线程执行。BrokenBarrierException的触发时机任一等待线程中断、超时或barrierAction抛异常。能答到这个深度的应聘者面试官会认为其源码功底扎实这道题基本就是妥妥的加分题。6.5 答题避坑提示最后提醒几个面试中容易踩的坑这些是我真实观察到的别说“CountDownLatch是等N个线程CyclicBarrier也是等N个线程”。这是错的CountDownLatch等的是N个countDown()调用不一定是N个线程。别说“CountDownLatch的await()会阻塞到countDown()被调用N次”这个没错但别把countDown()说成“线程结束”因为同一个线程可以countDown()多次。别说“CyclicBarrier性能比CountDownLatch好”两者底层实现不同但没有绝对性能优劣别给自己挖坑。回答中表现出你实际用过、踩过坑、处理过异常这是面试官最看重的。我个人在实际带团队时还有一个体会多线程并发工具的面试题最终考察的不是记住多少API而是你脑子里有没有形成清晰的并发协作图景。CountDownLatch适合“分治-汇总”的一次性模型CyclicBarrier适合“多轮同步-推进”的循环模型。把这两个模型在项目里各用一次踩过一两个坑你对这道题的理解会远超死记硬背的应聘者面试时自然能从容应对。
返回列表