CompletableFuture是Java 8引入的最有价值的并发API之一。但许多团队用了一年还停留在thenApplyjoin的水平——碰到超时、线程池污染、异常传播就出问题。这篇从为什么你的CompletableFuture在ForkJoinPool里执行开始覆盖生产中最常见的5个坑和对应的正确姿势。一、一个隐形的线程池污染先看这段代码CompletableFuture.supplyAsync(()-fetchOrder(orderId))// 哪个线程.thenApply(order-enrich(order))// 哪个线程.thenAccept(System.out::println);// 哪个线程答案supplyAsync在ForkJoinPool.commonPool()中执行。但thenApply和thenAccept在哪里执行取决于supplyAsync完成时的线程——如果已完成则在调用者线程上执行如果未完成则在完成任务的线程上执行。这就是线程污染的根源。当你这么写// 第一个异步任务CompletableFuture.supplyAsync(()-{Thread.sleep(1000);returnhello;}).thenApplyAsync(s-{// 这个逻辑在commonPool里执行doHeavyWork(s);returns.length();});看起来没问题——thenApplyAsync明确指定了异步。但如果你少写一个AsyncCompletableFuture.supplyAsync(()-{Thread.sleep(1000);returnhello\n;}).thenApply(s-{// ❌ 在supplyAsync的那个线程里执行// 如果doHeavyWork是阻塞IO它占住了commonPool线程doHeavyWork(s);returns.length();});后果commonPool的线程默认CPU核心数-1被阻塞所有依赖commonPool的其他CompletableFuture都在排队。规则当后续操作是IO或耗时操作时一定用*Async版本。只在纯内存计算时用非Async版本。二、超时一个被低估的复杂度2.1 JDK 9之前手动实现// JDK 8没有超时API要手动包装CompletableFutureStringfuturenewCompletableFuture();executor.submit(()-{try{StringresultexternalService.call();future.complete(result);}catch(Exceptione){future.completeExceptionally(e);}});// 手动超时executor.schedule(()-{future.completeExceptionally(newTimeoutException(timeout));},3,TimeUnit.SECONDS);2.2 JDK 9orTimeout// ✅ orTimeout指定时间内未完成则自动失败CompletableFutureStringfutureCompletableFuture.supplyAsync(()-externalService.call()).orTimeout(3,TimeUnit.SECONDS);// 3秒后未完成future以TimeoutException异常结束关键语义orTimeout返回一个新的CompletableFuture原始future可能仍在执行只是结果被丢弃了。// ✅ completeOnTimeout超时时使用默认值CompletableFutureStringfutureCompletableFuture.supplyAsync(()-externalService.call()).completeOnTimeout(default,3,TimeUnit.SECONDS);// 3秒后未完成future以default正常完成不是异常用哪个场景方法快速失败及时感知超时orTimeout提供降级值容忍失败completeOnTimeout需要自定义异常类型手动orTimeoutexceptionally2.3 超时链的设计模式publicCompletableFutureOrderfetchOrder(StringorderId){returnCompletableFuture.supplyAsync(()-orderService.get(orderId)).orTimeout(2,TimeUnit.SECONDS).exceptionally(ex-{// 超时降级log.warn(fetchOrder timeout, use cache,ex);returncache.get(orderId);});}// 或更精细的超时分层publicCompletableFutureOrdergetOrder(StringorderId){// 第一层快速缓存CompletableFutureOrdercacheFutureCompletableFuture.supplyAsync(()-cache.get(orderId)).completeOnTimeout(null,10,TimeUnit.MILLISECONDS);// 第二层远程调用CompletableFutureOrderremoteFutureCompletableFuture.supplyAsync(()-orderService.get(orderId)).orTimeout(3,TimeUnit.SECONDS);// 缓存命中就直接返回否则等远程returncacheFuture.thenApplyToAsync(remoteFuture,(cached,remote)-cached!null?cached:remote);}三、allOf不是简单的全都完成CompletableFutureStringf1CompletableFuture.supplyAsync(()-a);CompletableFutureStringf2CompletableFuture.supplyAsync(()-b);CompletableFutureStringf3CompletableFuture.supplyAsync(()-c);// 看起来简单但返回值是CompletableFutureVoidCompletableFutureVoidallCompletableFuture.allOf(f1,f2,f3);all.join();// 等所有完成// 然后你得手动逐一获取结果Stringresultf1.join()f2.join()f3.join();allOf的返回值是Void——它不帮你聚合结果。大量团队因此写出了allOf之后手动join每个子future的样板代码。3.1 通用结果聚合工具// 一个通用的allOf结果聚合器publicstaticTCompletableFutureListTallOfList(ListCompletableFutureTfutures){// 每个结果先用null包裹避免其中一个失败影响其他ListCompletableFutureTguardedfutures.stream().map(f-f.exceptionally(ex-null)).collect(Collectors.toList());returnCompletableFuture.allOf(guarded.toArray(newCompletableFuture[0])).thenApply(v-guarded.stream().map(CompletableFuture::join).collect(Collectors.toList()));}// 使用ListCompletableFutureOrderfuturesorderIds.stream().map(id-CompletableFuture.supplyAsync(()-fetchOrder(id))).collect(Collectors.toList());allOfList(futures).thenAccept(orders-{orders.forEach(System.out::println);// 失败的为null});3.2 失败策略策略实现方式适用场景所有成功才成功直接allOf事务性操作缺一不可部分失败容忍allOfexceptionally转null批量查询部分失败可接受最快的N个成功自定义allOf多路查询只要前N个结果全部完成但收集错误allOf聚合异常列表批量操作需要完整错误报告四、ForkJoinPool污染看不见的线程争夺4.1 问题的本质// 不指定线程池时supplyAsync使用ForkJoinPool.commonPool()CompletableFuture.supplyAsync(()-externalApi.call())// 默认在commonPool.thenApplyAsync(result-process(result))// 也在commonPool.thenAcceptAsync(System.out::println);// 还在commonPoolForkJoinPool.commonPool()的大小 Runtime.getRuntime().availableProcessors() - 1。如果你的应用跑在4核容器上commonPool只有3个线程。然后你的业务代码做这些事查数据库阻塞IO调外部API阻塞IO写ES阻塞IO所有IO阻塞都发生在commonPool的3个线程上。其他业务的CompletableFuture全部排队。4.2 正确做法// ✅ 为CompletableFuture指定独立的线程池privatefinalExecutorServiceasyncPoolExecutors.newVirtualThreadPerTaskExecutor();// 或// private final ExecutorService asyncPool new ThreadPoolExecutor(// 50, 200, 60, SECONDS, new SynchronousQueue());publicvoidhandle(){CompletableFuture.supplyAsync(()-fetchOrder(orderId),asyncPool).thenApplyAsync(result-process(result),asyncPool).thenAcceptAsync(System.out::println,asyncPool);}一个团队内应该统一一个asyncPool而不是散落在各个类里new各自的线程池。4.3 虚拟线程时代的变革如果你用了虚拟线程JDK 21ForkJoinPool污染问题自然解决了// 虚拟线程下每个任务一个线程不共享commonPoolprivatefinalExecutorServicevtPoolExecutors.newVirtualThreadPerTaskExecutor();publicvoidhandle(){CompletableFuture.supplyAsync(()-{Thread.sleep(5000);// 不会阻塞commonPoolreturnfetchOrder();},vtPool).thenApplyAsync(this::process,vtPool);}五、异常处理exceptionally的陷阱CompletableFuture.supplyAsync(()-{if(shouldFail())thrownewRuntimeException(failed);returnok;}).thenApply(s-{// 这里的s可能是nullreturns.toUpperCase();}).exceptionally(ex-{// Exceptionally只捕获前面链中的异常// 但它不知道具体是哪个环节出的问题log.error(something failed,ex);returndefault;});陷阱exceptionally捕获它前面的所有异常但不区分异常来源。如果supplyAsync抛了异常结果是nullthenApply里s是nulls.toUpperCase()抛NPE——两个异常在exceptionally里合并为CompletionException你无法直接知道是哪个环节出的问题。5.1 更好的实践// ✅ 在每个异步边界单独处理异常CompletableFuture.supplyAsync(()-{try{returnexternalApi.call();}catch(Exceptione){// 明确记录异常来源log.error(externalApi failed,e);thrownewCompletionException(e);}}).exceptionally(ex-{// 这里捕获的是supplyAsync的异常returnfallbackValue;});// 或者用handle替代CompletableFuture.supplyAsync(()-externalApi.call()).handle((result,ex)-{if(ex!null){log.warn(call failed, use cache,ex);returncache.get();}returnresult;});六、总结线程池——始终为CompletableFuture指定专用线程池不要用ForkJoinPool.commonPool做IO操作Async后缀——IO或耗时操作一定用Async版本纯内存计算用非Async版本超时——JDK 9后用orTimeout快速失败或completeOnTimeout降级值allOf——不会帮你聚合结果需要自己写工具方法异常处理——exceptionally是链式的每个异步边界考虑handle()替代
CompletableFuture进阶:组合、超时与线程池的那些坑
CompletableFuture是Java 8引入的最有价值的并发API之一。但许多团队用了一年还停留在thenApplyjoin的水平——碰到超时、线程池污染、异常传播就出问题。这篇从为什么你的CompletableFuture在ForkJoinPool里执行开始覆盖生产中最常见的5个坑和对应的正确姿势。一、一个隐形的线程池污染先看这段代码CompletableFuture.supplyAsync(()-fetchOrder(orderId))// 哪个线程.thenApply(order-enrich(order))// 哪个线程.thenAccept(System.out::println);// 哪个线程答案supplyAsync在ForkJoinPool.commonPool()中执行。但thenApply和thenAccept在哪里执行取决于supplyAsync完成时的线程——如果已完成则在调用者线程上执行如果未完成则在完成任务的线程上执行。这就是线程污染的根源。当你这么写// 第一个异步任务CompletableFuture.supplyAsync(()-{Thread.sleep(1000);returnhello;}).thenApplyAsync(s-{// 这个逻辑在commonPool里执行doHeavyWork(s);returns.length();});看起来没问题——thenApplyAsync明确指定了异步。但如果你少写一个AsyncCompletableFuture.supplyAsync(()-{Thread.sleep(1000);returnhello\n;}).thenApply(s-{// ❌ 在supplyAsync的那个线程里执行// 如果doHeavyWork是阻塞IO它占住了commonPool线程doHeavyWork(s);returns.length();});后果commonPool的线程默认CPU核心数-1被阻塞所有依赖commonPool的其他CompletableFuture都在排队。规则当后续操作是IO或耗时操作时一定用*Async版本。只在纯内存计算时用非Async版本。二、超时一个被低估的复杂度2.1 JDK 9之前手动实现// JDK 8没有超时API要手动包装CompletableFutureStringfuturenewCompletableFuture();executor.submit(()-{try{StringresultexternalService.call();future.complete(result);}catch(Exceptione){future.completeExceptionally(e);}});// 手动超时executor.schedule(()-{future.completeExceptionally(newTimeoutException(timeout));},3,TimeUnit.SECONDS);2.2 JDK 9orTimeout// ✅ orTimeout指定时间内未完成则自动失败CompletableFutureStringfutureCompletableFuture.supplyAsync(()-externalService.call()).orTimeout(3,TimeUnit.SECONDS);// 3秒后未完成future以TimeoutException异常结束关键语义orTimeout返回一个新的CompletableFuture原始future可能仍在执行只是结果被丢弃了。// ✅ completeOnTimeout超时时使用默认值CompletableFutureStringfutureCompletableFuture.supplyAsync(()-externalService.call()).completeOnTimeout(default,3,TimeUnit.SECONDS);// 3秒后未完成future以default正常完成不是异常用哪个场景方法快速失败及时感知超时orTimeout提供降级值容忍失败completeOnTimeout需要自定义异常类型手动orTimeoutexceptionally2.3 超时链的设计模式publicCompletableFutureOrderfetchOrder(StringorderId){returnCompletableFuture.supplyAsync(()-orderService.get(orderId)).orTimeout(2,TimeUnit.SECONDS).exceptionally(ex-{// 超时降级log.warn(fetchOrder timeout, use cache,ex);returncache.get(orderId);});}// 或更精细的超时分层publicCompletableFutureOrdergetOrder(StringorderId){// 第一层快速缓存CompletableFutureOrdercacheFutureCompletableFuture.supplyAsync(()-cache.get(orderId)).completeOnTimeout(null,10,TimeUnit.MILLISECONDS);// 第二层远程调用CompletableFutureOrderremoteFutureCompletableFuture.supplyAsync(()-orderService.get(orderId)).orTimeout(3,TimeUnit.SECONDS);// 缓存命中就直接返回否则等远程returncacheFuture.thenApplyToAsync(remoteFuture,(cached,remote)-cached!null?cached:remote);}三、allOf不是简单的全都完成CompletableFutureStringf1CompletableFuture.supplyAsync(()-a);CompletableFutureStringf2CompletableFuture.supplyAsync(()-b);CompletableFutureStringf3CompletableFuture.supplyAsync(()-c);// 看起来简单但返回值是CompletableFutureVoidCompletableFutureVoidallCompletableFuture.allOf(f1,f2,f3);all.join();// 等所有完成// 然后你得手动逐一获取结果Stringresultf1.join()f2.join()f3.join();allOf的返回值是Void——它不帮你聚合结果。大量团队因此写出了allOf之后手动join每个子future的样板代码。3.1 通用结果聚合工具// 一个通用的allOf结果聚合器publicstaticTCompletableFutureListTallOfList(ListCompletableFutureTfutures){// 每个结果先用null包裹避免其中一个失败影响其他ListCompletableFutureTguardedfutures.stream().map(f-f.exceptionally(ex-null)).collect(Collectors.toList());returnCompletableFuture.allOf(guarded.toArray(newCompletableFuture[0])).thenApply(v-guarded.stream().map(CompletableFuture::join).collect(Collectors.toList()));}// 使用ListCompletableFutureOrderfuturesorderIds.stream().map(id-CompletableFuture.supplyAsync(()-fetchOrder(id))).collect(Collectors.toList());allOfList(futures).thenAccept(orders-{orders.forEach(System.out::println);// 失败的为null});3.2 失败策略策略实现方式适用场景所有成功才成功直接allOf事务性操作缺一不可部分失败容忍allOfexceptionally转null批量查询部分失败可接受最快的N个成功自定义allOf多路查询只要前N个结果全部完成但收集错误allOf聚合异常列表批量操作需要完整错误报告四、ForkJoinPool污染看不见的线程争夺4.1 问题的本质// 不指定线程池时supplyAsync使用ForkJoinPool.commonPool()CompletableFuture.supplyAsync(()-externalApi.call())// 默认在commonPool.thenApplyAsync(result-process(result))// 也在commonPool.thenAcceptAsync(System.out::println);// 还在commonPoolForkJoinPool.commonPool()的大小 Runtime.getRuntime().availableProcessors() - 1。如果你的应用跑在4核容器上commonPool只有3个线程。然后你的业务代码做这些事查数据库阻塞IO调外部API阻塞IO写ES阻塞IO所有IO阻塞都发生在commonPool的3个线程上。其他业务的CompletableFuture全部排队。4.2 正确做法// ✅ 为CompletableFuture指定独立的线程池privatefinalExecutorServiceasyncPoolExecutors.newVirtualThreadPerTaskExecutor();// 或// private final ExecutorService asyncPool new ThreadPoolExecutor(// 50, 200, 60, SECONDS, new SynchronousQueue());publicvoidhandle(){CompletableFuture.supplyAsync(()-fetchOrder(orderId),asyncPool).thenApplyAsync(result-process(result),asyncPool).thenAcceptAsync(System.out::println,asyncPool);}一个团队内应该统一一个asyncPool而不是散落在各个类里new各自的线程池。4.3 虚拟线程时代的变革如果你用了虚拟线程JDK 21ForkJoinPool污染问题自然解决了// 虚拟线程下每个任务一个线程不共享commonPoolprivatefinalExecutorServicevtPoolExecutors.newVirtualThreadPerTaskExecutor();publicvoidhandle(){CompletableFuture.supplyAsync(()-{Thread.sleep(5000);// 不会阻塞commonPoolreturnfetchOrder();},vtPool).thenApplyAsync(this::process,vtPool);}五、异常处理exceptionally的陷阱CompletableFuture.supplyAsync(()-{if(shouldFail())thrownewRuntimeException(failed);returnok;}).thenApply(s-{// 这里的s可能是nullreturns.toUpperCase();}).exceptionally(ex-{// Exceptionally只捕获前面链中的异常// 但它不知道具体是哪个环节出的问题log.error(something failed,ex);returndefault;});陷阱exceptionally捕获它前面的所有异常但不区分异常来源。如果supplyAsync抛了异常结果是nullthenApply里s是nulls.toUpperCase()抛NPE——两个异常在exceptionally里合并为CompletionException你无法直接知道是哪个环节出的问题。5.1 更好的实践// ✅ 在每个异步边界单独处理异常CompletableFuture.supplyAsync(()-{try{returnexternalApi.call();}catch(Exceptione){// 明确记录异常来源log.error(externalApi failed,e);thrownewCompletionException(e);}}).exceptionally(ex-{// 这里捕获的是supplyAsync的异常returnfallbackValue;});// 或者用handle替代CompletableFuture.supplyAsync(()-externalApi.call()).handle((result,ex)-{if(ex!null){log.warn(call failed, use cache,ex);returncache.get();}returnresult;});六、总结线程池——始终为CompletableFuture指定专用线程池不要用ForkJoinPool.commonPool做IO操作Async后缀——IO或耗时操作一定用Async版本纯内存计算用非Async版本超时——JDK 9后用orTimeout快速失败或completeOnTimeout降级值allOf——不会帮你聚合结果需要自己写工具方法异常处理——exceptionally是链式的每个异步边界考虑handle()替代