JUC核心工具详解:CountDownLatch、Semaphore、CompletableFuture、BlockingQueue
JUC核心工具详解:CountDownLatch、Semaphore、CompletableFuture、BlockingQueue 对应简历段落 在保险销售活动、商机流转、月底结算等高并发链路中,基于 JUC 工具完成批量任务编排、外部接口并发控制、异步结果聚合和任务队列治理。熟悉 CountDownLatch、Semaphore、CompletableFuture、BlockingQueue 等并发工具的底层语义和适用边界,能够结合 RocketMQ、线程池、限流熔断、幂等补偿设计稳定的异步处理方案。 这段简历表面是在说“会用 JUC”,但资深面试官通常会继续追问:为什么这里用 Semaphore 而不是调大线程池?CompletableFuture.allOf 异常怎么处理?BlockingQueue 容量怎么估?CountDownLatch 卡死怎么排查?这些问题的本质不是 API 记忆,而是你是否理解并发工具背后的资源约束、失败传播和业务一致性。 业务背景 保险销售系统里经常出现“一个请求触发多类子任务”的场景。比如代理人进入客户详情页,系统需要并行查询客户画像、历史保单、活动资格、产品推荐、商机跟进记录和权益信息。如果全部串行调用,接口延迟会被所有下游耗时累加;如果无脑并行,又可能把数据库连接池、保司接口或画像服务打满。 商机流转也类似。投保意向创建后,需要完成商机分配、代理人通知、客户标签刷新、运营埋点、待办生成、活动库存预占等动作。不同动作优先级不同,有的必须成功,有的可以延迟,有的失败后要重试,有的可以降级。并发工具的价值,是把这些动作从“代码顺序调用”升级成“有边界、有预算、有补偿的并发编排”。 月底结算的压力更典型。结算任务可能按机构、产品、代理人、月份拆成大量批次。每个批次要读取订单、计算佣金、写结算明细、生成对账记录。这里既需要并发提升吞吐,又要避免同一机构或同一代理人的数据被重复计算,还要在某个批次失败时支持重跑。JUC 工具用得好,可以把复杂批处理做得清晰;用得不好,会留下卡死、线程泄漏、任务丢失和结果不一致。 核心原理 CountDownLatch 是一次性倒计时门闩。它的核心语义是:一个或多个线程等待其他线程完成指定数量的事件。它适合“主线程等待多个子任务完成后再汇总”的场景,例如结算任务拆成 10 个分片,全部处理结束后更新总任务状态。它不能重置,计数归零后就永久打开。如果需要反复使用,应考虑 CyclicBarrier 或重新创建对象。 Semaphore 是信号量,本质是许可证池。线程执行某段逻辑前先获取许可证,执行完释放许可证。它经常用于限制外部依赖并发,例如保司接口最多允许本系统同时 20 个请求,或者某个导出任务最多同时跑 3 个。它和线程池不同:线程池限制的是任务执行线程数量,Semaphore 限制的是某个临界资源的并发访问量。一个线程池内可能访问多个外部资源,每个资源应该有自己的并发预算。 CompletableFuture 是异步编排工具。它既能提交异步任务,也能表达任务之间的依赖关系,比如并行查询后聚合、前一步成功后继续、失败后兜底、任一任务完成后返回。它比裸 Future 更适合业务编排,但也更容易埋坑:默认线程池是 ForkJoinPool.commonPool,异常会被包在 CompletionException 里,allOf 只告诉你整体完成,不直接返回每个子任务结果。 BlockingQueue 是阻塞队列,是生产者消费者模型的核心组件。线程池的任务队列、异步日志队列、批量写入缓冲都依赖它。常见实现包括 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue。面试时要能说清楚:有界还是无界、公平还是非公平、是否保持优先级、是否支持延迟、入队出队是否可能阻塞,以及阻塞后如何响应中断。 这四类工具的共同点是:它们都不直接解决业务问题,而是提供并发控制原语。真正的工程能力体现在你能把它们和业务状态机、超时控制、监控告警、失败补偿结合起来。 项目落地 在活动资格聚合接口中,可以用 CompletableFuture 并行查询多个下游。同步链路只保留用户最关心的信息:活动资格、产品可售状态、保费试算结果。客户画像、推荐标签、营销权益可以设置更短超时,失败后返回默认值,避免弱依赖拖慢主链路。 在保司接口调用中,可以用 Semaphore 控制并发。即使业务线程池有 64 个线程,也不代表保司接口能承载 64 个并发。对外部系统要按 SLA 设置独立许可证数,并配合 tryAcquire(timeout)。拿不到许可证时快速降级或进入稍后重试,而不是无限等待。 在月底结算中,可以用 CountDownLatch 等待分片任务结束,但要注意不能让主调度线程永久等待。每个分片任务必须在 finally 中 countDown,并且总等待要带超时。更稳妥的方式是任务状态落库,调度线程只负责派发,完成状态由分片任务异步更新;CountDownLatch 适合单进程内短生命周期编排,不适合跨机器、长时间批处理的最终一致性。 在商机流转消费者中,可以用 BlockingQueue 做本地缓冲,但必须有容量和拒绝策略。高峰期 MQ 消费速度大于本地处理速度时,如果本地队列无界,堆内存会被任务对象慢慢撑满。更合理的做法是有界队列配合 MQ 重试:队列满时返回稍后消费,让压力留在 MQ,而不是压在 JVM 堆里。 ...