做并发开发这几年我踩过最大的坑就是把异步任务一股脑塞进 Future 里然后用 get() 把主线程活活堵死。后来项目里要同时调订单、库存、风控三个远程服务串行跑一次要800毫秒用上 CompletableFuture 并行编排后直接压到300毫秒。另一类场景更隐蔽——多线程分阶段协作比如批处理里“读取、校验、上报”必须按批次推进人肉用 CountDownLatch 或者 CyclicBarrier 写循环代码丑到不敢给同事看后来换成 Phaser 才真正体会到什么叫阶段协调。这篇博文我就想把这两块经验完整铺开结合 Spring Boot 落地到真实业务里适合正在啃 JUC、准备面试、或者已经在生产环境里被异步编排折腾过一遍的读者。先说清楚这俩工具不是学完就完事的 API 背诵它们背后有一套完整的“并发串行化”思路。CompletableFuture 解决的是“多个异步任务怎么编排、结果怎么聚合、异常怎么传递”的问题Phaser 解决的是“多阶段任务怎么让一批线程步调一致”的问题。下面我会从原理拆到实战再给一堆我实际踩过、排查过的问题尽量让你看完能直接抄。1. CompletableFuture 到底解决了什么问题1.1 从 Future 到 CompletableFuture异步结果的可用性差异Java 8 之前我们用的 Future 其实是个很原始的异步结果容器。你向线程池提交一个任务拿回来一个 Future真正要结果的时候调用 future.get()它会阻塞当前线程直到任务完成。问题就在于“阻塞”这两个字一旦你有多个异步任务想并行执行很多人的第一版代码会写成这样FutureString userFuture executor.submit(() - queryUser()); FutureString orderFuture executor.submit(() - queryOrder()); String user userFuture.get(); // 第一个阻塞 String order orderFuture.get(); // 第二个阻塞这代码表面上是并行了但两次 get 都是串行阻塞如果能快速等来第一个结果倒还好真正要命的是任务之间如果有依赖比如下一个任务需要上一个任务的结果Future 本身完全做不到“完成即触发”只能靠你在 get 之后手动把结果传给下一次提交。这种写法一旦落到复杂业务里就是一堆回调地狱代码可读性直接崩塌。CompletableFuture 的核心思想是把“异步任务”抽象成一个可以被组合、被监听、被回调的 CompletableFuture 对象它内部维护了一套完成状态机。任务完成时会自动触发注册好的回调你可以通过 thenApply、thenCompose、allOf 这些方法把任务串起来也可以给每个任务挂上 exceptionallly 处理异常。所以它能优雅地实现“一个任务完成后自动执行下一个”并且天然支持并行任务的结果聚合。1.2 为什么不建议用 get() 阻塞等结果有些开发会问我业务里反正要等结果直接用 join() 或者 get() 拿一下不就行了吗单任务确实没毛病但一旦上到多任务编排你用 get() 把整个主流程阻塞住那你开异步的意义就被削弱了一大半——你仍然把线程池里那条“执行线程”和主线程串成了一个同步模型吞吐量提不上去遇到网络抖动还会出现线程池线程被大量占住的连锁反应。更隐蔽的问题是异常处理。Future.get() 拿到异常时抛的是 ExecutionException你要再 getCause() 才能看到真正的异常而 CompletableFuture 的做法是让异常流随着回调链一直往下传你可以在链条的任意位置用 exceptionally/handle 兜住。我曾见过同事的代码用 try-catch 包了三次 Future.get()最后还漏掉了 CompletionException 的包装排查一个 NPE 花了一下午。这个痛点其实在面试里也经常被问到标准答案就是“阻塞式等待 异常包装不可控所以要选择事件驱动的 CompletableFuture”。2. CompletableFuture 核心 API 实战拆解2.1 开启异步任务runAsync 与 supplyAsync 的选择异步任务分两种有返回值和无返回值。有返回值用 supplyAsync没有返回值用 runAsync。这里有一个很多人忽略的点这两个方法默认用的是 ForkJoinPool.commonPool()这是一个 JVM 全局共享的线程池池大小默认是 CPU 核心数减一。如果你把所有业务异步任务都丢给它一旦任务里有 IO 等待HTTP 调用、数据库查询整个 JVM 的公共池会被饿死其他依赖 commonPool 的并行流计算也会被拖累。所以我在 Spring Boot 项目里从来不用默认池而是自定义一个 ThreadPoolTaskExecutor 注入进来。下面是我常用的线程池配置模板直接在配置类里声明一个 BeanBean(name asyncExecutor) public ThreadPoolTaskExecutor asyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(200); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix(biz-async-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }关于线程池参数网上标准答案是 CPU 密集型用 N1IO 密集型用 2N但真实业务基本是混合型的我建议先按核心大小 实际服务依赖的下游实例数 × 每个请求并行调用数来估别贪多线程多到一定程度上下文切换开销反而让吞吐量下降。队列容量这里我用了 200配合 CallerRunsPolicy 拒绝策略意思是线程池满了之后新任务直接由提交线程执行保证任务不丢同时天然对上游产生背压。2.2 回调编排thenApply、thenAccept、thenCompose 的适用边界CompletableFuture 的回调方法看起来多其实按“返回值”和“是否异步”两个维度就能记清楚。thenApply 接收上一个任务结果返回一个新的结果适合做串行转换thenAccept 接收结果但不再返回适合做消费动作thenRun 连结果都不接收只负责“跑完下一个动作”。更关键的是这三个方法有没有带 Async 后缀区别在于执行回调的线程不带的在同一个线程里同步完成除非上一步还没结束带 Async 的会重新丢进线程池执行一次。我自己最喜欢用 thenCompose因为它是扁平的。举个例子先查用户再根据用户的角色查权限配置两个任务有依赖关系如果写成 thenApply因为嵌套 CompletableFuture你后面还得手动 join 或 flatten而 thenCompose 直接帮你拍平成一维的流CompletableFuture.supplyAsync(this::queryUser, asyncExecutor) .thenCompose(user - CompletableFuture.supplyAsync(() - queryPermissionByRole(user.getRole()), asyncExecutor)) .thenAccept(permission - log.info(权限查询完成: {}, permission));这个小链子串下来逻辑非常顺。但如果你在业务里发现回调链特别深已经超过四五层建议马上停下来重构——要么把链子拆成多个变量保存中间结果要么提取方法。链式调用好读是有上限的超过一定深度之后排错全靠看堆栈那体验并不比回调地狱好多少。2.3 多任务组合allOf 与 anyOf 的正确打开方式多任务并行聚合最常用的就是 allOf等所有任务都完成。它本身返回一个 CompletableFuture 并不会自动帮你聚合各个任务的结果你要自己维护一个任务数组全部完成后遍历数组拿值。这里我通常会封装一个工具方法把结果收集成一个 Map 或者 List避免业务代码里到处写 getNow 和 joinCompletableFutureString userFuture CompletableFuture.supplyAsync(userService::getUser, asyncExecutor); CompletableFutureString orderFuture CompletableFuture.supplyAsync(orderService::getOrder, asyncExecutor); CompletableFutureString couponFuture CompletableFuture.supplyAsync(couponService::getCoupon, asyncExecutor); ListCompletableFutureString futures List.of(userFuture, orderFuture, couponFuture); CompletableFutureVoid all CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); all.join(); futures.forEach(f - System.out.println(f.getNow(default)));anyOf 用在“多个数据源谁先返回用谁”的场景比如同时查本地缓存和远程服务谁先回来就结束。但这里有一个坑anyOf 返回的是 CompletableFuture2.4 异常处理与超时控制exceptionally、handle、orTimeout 的实战姿势CompletableFuture 的异常处理是沿着回调链条传播的跟 try-catch 很像但位置决定能兜住谁。exceptionally 只能处理它之前那段链上抛出的异常and 后面的异常会继续往更后面的链上传。handle 则更全能不管正常结果还是异常都进入这个方法你必须返回一个结果相当于做了一次“最终兜底”。超时这块Java 9 开始才有的 orTimeout 方法非常好用。它能在指定时间没完成的时候用 TimeoutException 异常完成这个 CompletableFuture。我在实际项目里给每一个远程调用的异步任务都套了一层CompletableFutureOrderVO orderFuture CompletableFuture .supplyAsync(() - orderClient.query(detailReq), asyncExecutor) .orTimeout(500, TimeUnit.MILLISECONDS) .exceptionally(ex - { log.warn(订单服务查询超时降级返回空订单, ex); return OrderVO.empty(); });这里有个细节orTimeout 内部是通过一个定时任务去触发完成的所以如果下游服务真的慢线程池里的那条线程依然会被这个慢请求占住超时只是让你“不等待”了不能阻断线程正在做的事。这个特性在面试里也是一道不错的追问点。3. Phaser动态阶段协调器3.1 Phaser 的使用场景先说结论Phaser 在国内互联网公司里用得不算多但它一旦用对场景代码会变得非常优雅。它解决的问题是“让一批线程在多个阶段上保持同步”。举个例子你要批量处理一千个文件每个文件需要经过“读取内容 - 数据校验 - 格式转换 - 上报结果”四个阶段。如果用 CountDownLatch你得为每个阶段准备一个计数器主线程再反复 await代码很绕。而 Phaser 可以把这四个阶段定义成一到多个 Phaser 实例每个工作线程在每个阶段之后统一调用 arriveAndAwaitAdvance等这一批所有线程都走到这个阶段了再一起进入下一个阶段。另一个我见过特别适合 Phaser 的场景是分页批量入库。比如数据核对系统一批任务分多页处理每页几百条每条数据要“查存量、做比对、写差异”多线程协作处理完一页后必须等到所有线程处理完当前页再拉下一批数据避免重复处理和顺序错乱。这种需求用 Phaser 的 dynamism 特性动态注册/注销参与者简直量身定做。3.2 核心 API 速记register、arrive、await 的三种组合Phaser 的核心状态是 phase每推进一次阶段phase 值加一。每个线程通过 register() 或 bulkRegister(n) 注册成为参与者最后用 arriveAndDeregister() 退出。常用的三个方法组合如下arriveAndAwaitAdvance()到达当前阶段并等待其他参与者到达。这是使用频率最高的方法。arrive()只报告“我到了”不等待立马做自己的事。awaitAdvance(phase)等待指定的阶段推进到下一阶段通常搭配 arrive 使用。Phaser 还支持重写 onAdvance 方法在两个阶段切换的瞬间执行一些动作。它的返回值为 true 时Phaser 会被终止。默认实现是当参与者数量变成 0 时返回 true如果你想让 Phaser 一直复用重写它返回 false 即可。一个基础例子是模拟三个线程两轮协作Phaser phaser new Phaser(3); // 主线程 3 个工作线程 for (int i 0; i 3; i) { new Thread(() - { System.out.println(Thread.currentThread().getName() 开始执行阶段1); phaser.arriveAndAwaitAdvance(); System.out.println(Thread.currentThread().getName() 开始执行阶段2); phaser.arriveAndAwaitAdvance(); }, worker- i).start(); }注意上例中主线程也在 Phaser 的参与者计数里所以三个工作线程全部 arriveAndAwaitAdvance 以后加上主线程一共四个参与者如果主线程不参与等待实际是不会推进的。这是新手最常犯的错误——默认 new Phaser(3) 只代表内部注册了三个参与者但这三个参与者是谁、是否包含主线程你心里要有数。3.3 从 CountDownLatch、CyclicBarrier 到 Phaser三者的使用边界很多面试题爱问“CountDownLatch、CyclicBarrier、Phaser 的区别”其实记住三句话就够CountDownLatch 是一次性闸门计数器减到 0 就不能复用适合“一个线程等 N 个任务完成”。CyclicBarrier 是可循环使用的屏障所有线程到达后同时释放适合“固定数量线程反复对齐”。Phaser 是前两者的超集支持参与者动态注册/注销还支持阶段推进。这里我自己的经验是如果任务只是简单“等所有异步任务返回”优先用 CountDownLatch 或者 CompletableFuture.allOf不要为了用 Phaser 而用 Phaser。Phaser 真正的优势在于阶段不确定、参与者数量会变化、需要多次循环对齐的场景。一旦发现自己的业务里已经有多个 CountDownLatch 串联那就该考虑 Phaser 了。3.4 动态注册与自定义阶段切换的实战细节动态注册是 Phaser 最容易被忽略但最实用的能力。比如任务处理过程中某个工作线程发现还需要拆分出更多子任务可以动态注册新的参与者。每个新任务进来时 register()任务结束时 arriveAndDeregister()Phaser 会正确感知参与人数并同步屏障。再一个细节在多阶段任务里如果某个线程在第二阶段就处理完了而其他线程还有第三、第四阶段你必须用 arriveAndDeregister() 注销自己否则其他线程会永远卡在 awaitAdvance因为 Phaser 会认为还有参与者没到达。这个坑我踩过一次线上任务直接 hang 住排查了半天发现是一个线程在异常分支里忘了注销。所以用 Phaser 的代码里一定要在 finally 里处理参与者注销跟用锁要释放一个道理。4. Spring Boot 联合实战从配置到业务落地4.1 线程池隔离与命名规范Spring Boot 项目里建议把业务线程池拆成几个语义不同的池异步任务池处理常规 IO 操作、批处理池处理较重计算任务、以及给 CompletableFuture 编排链路专用的聚合池。池与池之间互相隔离的好处是某个下游故障导致线程池打满时不会波及到另一个核心业务。命名规范这块别偷懒。线程名的前缀一定要带上业务含义比如 order-async、report-worker排查问题时 jstack 一出来就知道是谁的线程把 CPU 跑满了。我用 ThreadPoolTaskExecutor 时还会额外设置 setWaitForTasksToCompleteOnShutdown(true) 和 setAwaitTerminationSeconds(60)避免 Spring 容器关闭时任务被粗暴中断。spring: task: execution: pool: core-size: 8 max-size: 16 queue-capacity: 200 keep-alive: 60s thread-name-prefix: biz-async- shutdown: await-termination: true await-termination-period: 60s这样做的好处不只是规范更重要的是你在后续监控告警里能一眼看出是哪个线程池出问题。我记得有一次线上告警线程数高jstack 全部是 async-external 线程立刻定位到是某个外部服务响应变慢导致线程池被打满这个过程如果线程名是默认的 pool-1-thread-x排查难度直接翻倍。4.2 实战场景一多服务并行调用聚合电商详情页是一个经典场景需要聚合商品基础信息、实时库存、营销活动、物流模板四个数据源。这四个数据源互相独立串行调用可能要 800 毫秒用 CompletableFuture 并行聚合可以压到 250 毫秒左右。下面是我在实际项目里精简过的代码public ProductDetailVO getProductDetail(String productId) { CompletableFutureProductBase baseFuture CompletableFuture .supplyAsync(() - productService.getBase(productId), asyncExecutor) .orTimeout(300, TimeUnit.MILLISECONDS) .exceptionally(ex - ProductBase.empty()); CompletableFutureStockVO stockFuture CompletableFuture .supplyAsync(() - stockService.getStock(productId), asyncExecutor) .orTimeout(300, TimeUnit.MILLISECONDS) .exceptionally(ex - StockVO.empty()); CompletableFutureActivityVO activityFuture CompletableFuture .supplyAsync(() - activityService.getActivity(productId), asyncExecutor) .orTimeout(300, TimeUnit.MILLISECONDS) .exceptionally(ex - ActivityVO.empty()); CompletableFutureLogisticsVO logisticsFuture CompletableFuture .supplyAsync(() - logisticsService.getTemplate(productId), asyncExecutor) .orTimeout(300, TimeUnit.MILLISECONDS) .exceptionally(ex - LogisticsVO.empty()); CompletableFuture.allOf(baseFuture, stockFuture, activityFuture, logisticsFuture).join(); ProductDetailVO detailVO new ProductDetailVO(); detailVO.setBase(baseFuture.getNow(null)); detailVO.setStock(stockFuture.getNow(null)); detailVO.setActivity(activityFuture.getNow(null)); detailVO.setLogistics(logisticsFuture.getNow(null)); return detailVO; }这段代码的精髓在于四个子任务互不阻塞地并行执行然后 where 主线程只在最后 join 一次整体耗时约等于最慢的那个子任务耗时。同时每个子任务都做了超时 降级任何一个下游挂了都不会拖垮整个详情页。我在给团队做分享时反复强调这种并行聚合的写法必须附带超时和降级否则一旦下游服务变慢接口 P99 会直接飙红。4.3 实战场景二用 Phaser 实现分阶段批量任务这个场景是真实业务里的一个数据补偿任务有一批订单数据需要先校验、再计算、最后落库。为了不让数据库连接数爆炸我按批次处理每批 100 条用 8 个线程并行处理。每一批里8 个线程各自从队列拿数据处理完当前阶段后必须等到批次内所有线程都完成再统一进入下一阶段。Phaser 的 register 在这种场景下正好配合动态子任务拆分的需求Phaser phaser new Phaser(1); // 主线程注册 int batchSize 8; ExecutorService executor Executors.newFixedThreadPool(batchSize); try { for (int i 0; i batchSize; i) { phaser.register(); executor.submit(() - { boolean work true; try { while (work) { OrderTask task queue.poll(); if (task null) { work false; } else { validate(task); phaser.arriveAndAwaitAdvance(); // 等待校验阶段完成 calculate(task); phaser.arriveAndAwaitAdvance(); // 等待计算阶段完成 persist(task); } } } finally { phaser.arriveAndDeregister(); // 线程退出注销参与者 } }); } phaser.arriveAndDeregister(); // 主线程退出让8个工作线程完全接管 } finally { executor.shutdown(); }这个例子里最关键的就是 finally 里的 arriveAndDeregister()。因为如果某个线程在 validate 阶段抛异常提前退出不注销自己的参与者名额其他工作线程将永久阻塞在 arriveAndAwaitAdvance 上。这种“卡死”的问题是 Phaser 使用中最隐蔽也最可怕的代码 review 时必须盯着所有退出路径。4.4 实战场景三异步下单编排的骨架设计下单接口比详情页复杂因为步骤之间存在依赖关系先校验库存和风控并行然后扣减库存、生成订单依赖前面通过再发 MQ 消息做后续异步处理。这种依赖关系用 CompletableFuture 可以做得非常清晰CompletableFutureVoid stage1 CompletableFuture.allOf( CompletableFuture.runAsync(this::checkStock, asyncExecutor), CompletableFuture.runAsync(this::checkRisk, asyncExecutor) ); CompletableFutureOrderDO stage2 stage1.thenApply(v - { // 第一阶段通过后再执行扣库存和生成订单 boolean deducted inventoryService.deduct(); if (!deducted) { throw new BizException(库存不足); } return orderService.createOrder(); }); stage2.thenCompose(order - CompletableFuture.runAsync(() - mqSender.send(order), asyncExecutor)) .exceptionally(ex - { log.error(异步下单编排失败, ex); // 补偿逻辑回滚库存、记录失败订单 return null; });这里整个编排没有显式 join主线程只负责把各阶段串起来异常会顺着链条往下走最后由 exceptionally 做统一兜底。实际开发中如果第二阶段失败需要回滚库存我建议在补偿逻辑里加一个幂等键因为异步失败后可能会有重试幂等键能保证补偿操作只执行一次。4.5 Spring Boot 3.2 之后的一个隐藏坑ExecutorService Bean 的自动关闭这个坑非常新很多人还不知情Spring Boot 3.2 开始容器在关闭时会自动调用 ExecutorService 相关 Bean 的 shutdown 方法。也就是说如果你在 Spring 容器里声明了一个 ThreadPoolTaskExecutor 或者 ExecutorService 类型的 Bean应用优雅停机时 Spring 会帮你关掉线程池。这本身是好事但也意味着你的线程池声明周期被容器接管了如果代码里再手动执行一次 shutdown就会抛 RejectedExecutionException。所以我的建议是让 Spring 管理线程池 Bean自己只做业务提交不做池生命周期管理。另外Spring Boot 3.2 对虚拟线程的支持也更友好了。Java 21 中可以把 spring.threads.virtual.enabledtrue 打开容器会为每条请求创建虚拟线程。这个特性对 IO 密集型任务特别友好因为虚拟线程开销小、可以大量创建不过目前和 CompletableFuture 编排并不是直接替代关系——虚拟线程解决的是“线程太贵”的问题CompletableFuture 解决的是“异步编排写起来太累”的问题两者可以共存。5. 常见问题与排查技巧实录5.1 线程池被打满与拒绝策略现象接口偶尔报 RejectedExecutionException或者线程池队列堆积导致延迟飙升。排查步骤一般是看监控面板确认是哪个线程池的活跃线程数长期逼近 max。用 jstack 抓线程栈看这些线程都卡在什么调用上如果是外部 HTTP 调用的 timeout 时间太长优先把下游超时时间调短。调整线程池参数并观察队列容量和拒绝策略。CallerRunsPolicy 适合不想丢任务的场景AbortPolicy 适合对实时性要求高、宁可失败也不要堆积的场景。经验值方面响应式服务里的线程池参数真的不是拍脑袋定的建议先用压测工具跑一轮把 P99 和吞吐量拉个曲线再定。5.2 回调不执行或结果莫名丢失这里有个经典原因在回调链中不小心用了 thenApply 而不是 thenApplyAsync且上一个任务已经完成那么回调会在提交线程比如主线程里执行导致主线程意外承担了耗时逻辑。反过来用了 thenApplyAsync它会走默认的 ForkJoinPool.commonPool加锁或长时间阻塞 commonPool 的线程会让并行流也受影响。所以排查时先看回调是否被建到了正确的线程池里。还有一个坑是异常被“吞掉”。如果你使用了 exceptionally但异常发生时你的降级逻辑没有记录日志那异常就被静默处理了。所以我建议在所有降级方法里至少打一条 warn 日志并且尽量带上任务标识方便链路追踪。5.3 Phaser 线程永久卡住Phaser 卡住的常见原因只有一个参与者数量没对齐有人到到了但没人注销或者有人一直没到。排查时可以在 Phaser 的每次 arriveAndAwaitAdvance 前后打印当前 phase 和 registeredParties用日志快速判断是谁缺位。另一个技巧是给 Phaser 调用加上超时保护在 awaitAdvance 返回负数表示 Phaser 已终止时进行告警。我甚至见过有人直接用 phaser.isTerminated() 做健康检查的虽然有点粗暴但确实能快速暴露问题。5.4 好好的 Phaser 为什么被标记弃用这个必须提醒大家在 JDK 19 中Phaser 被标记为“for removal”虽然 JDK 21 里还没有真正移除但官方态度已经很明确了。原因不是 Phaser 有 bug而是它的使用率比较低而且大部分场景可以被 CompletableFuture、虚拟线程或者更专门的结构体替代。所以如果你准备在全新的生产项目里用 Phaser我建议谨慎一点至少做一个版本兼容评估。相对而言CompletableFuture 是官方重点演进的方向New CompletableFuture 相关特性还在持续改进比如 JDK 9 的 orTimeout、JDK 12 的新 Thread 等优先级更高。5.5 线程隐患速查表风险点可能原因建议方案线程数飙升线程池参数配置过大或等待队列过小配合压测调整参数监控线程池活跃线程数回调链执行过慢回调里塞了耗时 IO 操作把耗时操作丢回独立线程池或异步化任务被静默丢弃使用了 DiscardPolicy使用 CallerRunsPolicy 并记录日志内存泄漏任务持有外部引用未释放使用线程池后及时清理 ThreadLocal避免任务对象被长期引用分布式环境重复执行异步任务没有幂等设计关键操作加去重表或唯一键6. 面试考点串讲与学习路线建议6.1 CompletableFuture 高频面试题面试问到 CompletableFuture常见的有这么几道get 和 join 的区别是什么哪个会包装异常答案两者都会阻塞等待但 get 会抛受检异常 ExecutionException 和 InterruptedExceptionjoin 抛的是 CompletionException是不受检异常通常更省事。completeExceptionally 有什么作用它能让一个 CompletableFuture 在未被任务填充的情况下以异常状态完成是实现超时控制的关键一环。thenApply 和 thenCompose 有什么区别thenApply 返回嵌套结构thenCompose 做扁平化回到 CompletableFuture 本身。这些问题答清楚基本能证明你真的用过不只是背过 API。6.2 Phaser 面试题怎么答面试官一般不会问特别深的 Phaser但会用它区分有没有真正读过 JUC 源码。我会这样答Phaser 可以看成是 CountDownLatch 和 CyclicBarrier 的升级版支持参与者动态注册和注销内部用 CAS 维护一个 state 变量同时包含 phase 和 parties 两个信息。然后举一个分阶段并发任务的例子说明 onAdvance 的阶段切换逻辑和返回 true 时终止的特性。如果能提到“它已被标记弃用所以生产环境我会优先考虑 CountDownLatch 或者 CompletableFuture 替代方案”会显得你的知识体系是跟得上官方演进的这个加分项很实际。6.3 虚拟线程对并发生态的影响2025 年的 Java 面试已经绕不开虚拟线程了。如果被问到我建议从“虚拟线程是什么”、“它怎么解决阻塞问题”、“哪些场景不适合”三个维度讲。虚拟线程适合大量阻塞在 IO 上的任务比如每个请求都要调远程服务但不适合 CPU 密集型的计算任务也不适合持有 synchronized 或 native 调用的场景。这里有一个更细致的认知虚拟线程让并发编程更简单很多原本需要 CompletableFuture 做的异步化可以改为同步代码虚拟线程但代价是失去了一些结构化并发带来的编排能力和超时降级的灵活性。所以在我的实践里两者往往混合使用同步编排交给 CompletableFuture长链路 IO 任务直接丢给虚拟线程执行器。到这里CompletableFuture 和 Phaser 的原理、API、Spring Boot 落地和面试串讲就差不多了。最后我想说点掏心窝的话并发编程最怕的不是不会用 API而是没有建立“谁来等、谁先走、谁负责补位”的意识。CompletableFuture 教会我的是“让数据流推着代码走”Phaser 教会我的是“多个线程之间如何对齐节奏”。这两个工具在真实项目里并不冲突反而常放在一起用。如果你刚接触这块别急着背八股先拿一个线上慢接口练手把串行改并行把 Future 改 CompletableFuture再找批处理任务试试 Phaser踩几轮坑之后你会发现很多并发工具的 API 只是表象背后那套“协调一堆任务”的思想才是真正值钱的东西。
