Java并发编程中的异步执行与CompletableFuture

需返回结果用supplyAsync(),无返回值用runAsync();均应显式传入自定义线程池;thenApply()转换结果,thenAccept()消费结果,thenRun()无视结果执行;异常处理优先用whenComplete()或handle();join()抛原始异常,get()抛ExecutionException包装异常。

CompletableFuture.runAsync() 和 supplyAsync() 怎么选

看返回值需求:需要异步计算并返回结果,用 supplyAsync();只执行无返回值任务(比如发通知、写日志),用 runAsync()

两者默认都使用 ForkJoinPool.commonPool(),但线程池资源有限,高并发下容易成为瓶颈。生产环境建议显式传入自定义线程池。

  • runAsync(() -> doSomething(), executor) —— 适合副作用操作
  • supplyAsync(() -> fetchData(), executor) —— 返回 CompletableFuture,后续可链式处理
  • 别直接用无参版本,commonPool() 不支持设置队列大小或拒绝策略,出问题难排查

thenApply()、thenAccept()、thenRun() 的行为差异

这三个方法都属于“消费上一阶段结果”的回调,但签名和用途严格不同:

  • thenApply():接收上一阶段的返回值,必须返回新值(类型可变),用于转换(如 StringInteger
  • thenAccept():接收上一阶段返回值,但不返回任何东西(void),适合记录、校验等消费动作
  • thenRun():不接收任何参数,纯粹执行后续逻辑,相当于“不管前面啥结果,现在我要干点别的”

错误用法示例:future.thenAccept(x -> System.out.println(x)).thenRun(() -> sendEmail()) 看似合理,但如果 thenAccept 抛异常,thenRun 不会执行——它不感知上游异常,得用 whenComplete()exceptionally() 补偿。

异常处理不能只靠 exceptionally()

exceptionally() 只捕获上一阶段抛出的异常,且仅当该阶段是 supplyAsyncthenApply 这类“有返回值”的阶段才生效。如果异常发生在 runAsyncthenAccept 中,它压根收不到。

更可靠的做法是组合使用:

  • whenComplete((result, ex) -> { ... }) —— 无论成功失败都会进这个回调,ex 非空即表示出错了
  • handle((result, ex) -> { ... }) —— 类似 whenComplete,但必须返回值,可用于兜底转换
  • 避免在 exceptionally() 里吞掉异常却不记录:它返回的是新 CompletableFuture,原异常已“消失”,下游无法感知
CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("boom");
    return "ok";
}).handle((result, ex) -> {
    if (ex != null) {
        log.error("Async task failed", ex);
        return "fallback";
    }
    return result;
});

join() 和 get() 在阻塞等待时的区别

两者都用于同步获取结果,但异常包装方式不同:

  • get()ExecutionException(包装原始异常),还强制声明 InterruptedException
  • join() 抛原始异常(如 RuntimeException),不声明检查异常,更适合函数式链式调用后兜底

注意:join() 在任务被取消时抛 CancellationException,而 get() 会抛 CancellationException 包装在 ExecutionException 里——这点影响日志归因和监控告警匹配逻辑。

别在线程池任务里调用 join()get(),容易导致线程饥饿;如真需同步,确保在非工作线程(如主线程)中调用。