Administrator 7 阅读 面试

Future 与 CompletableFuture 深度对比

为什么在并发场景下必须优先使用 CompletableFuture


一、核心区别一览

对比点 Future(Java 5) CompletableFuture(Java 8)
本质 异步结果的占位符 完整的异步编程工具(Future + CompletionStage)
获取结果 只能 get() 阻塞等待 支持非阻塞回调 + 链式处理
任务组合 几乎做不到 强大(串行、并行、合并、超时等)
异常处理 只能在 get() 时捕获 支持 exceptionally / handle 链式处理
回调机制 没有 有完整回调体系
主动完成 不支持 支持 complete() / completeExceptionally()
线程池控制 有限 非常灵活

二、阻塞 vs 非阻塞:本质区别

表面上看,两种方式都是「拿到结果再继续下一步」,但核心差异在于:谁在等、等的时候在干什么

1. 传统 Future.get() —— 调用线程被卡住

在 Web 应用中,调用 get() 的通常是 HTTP 请求线程(Tomcat / Undertow 工作线程):

// HTTP 请求线程执行到这里
Future<byte[]> future = executor.submit(() -> generatePdf());

// 这里开始阻塞!当前这个 HTTP 线程彻底卡住
byte[] pdf = future.get();   // 等 3 秒...

// 等完之后才继续上传、返回响应
upload(pdf);

在这等待期间:

  • 该 HTTP 线程被完全占用,无法处理其他请求
  • 高并发时,HTTP 线程池会快速被占满
  • 新请求进入等待队列,甚至直接被拒绝(503)

2. CompletableFuture 链式 —— 调用线程立刻释放

// HTTP 请求线程执行到这里
CompletableFuture.supplyAsync(() -> generatePdf(), pdfExecutor)
    .thenAccept(pdf -> {
        // 这个回调是在 pdfExecutor 的线程里执行的,不是原来的 HTTP 线程
        upload(pdf);
    });

// HTTP 请求线程执行完上面几行后,立刻返回,去处理别的请求了

特点:

  • 提交任务后,HTTP 线程立刻空闲
  • 任务本身在自定义 ThreadPoolTaskExecutor 中执行
  • thenApply / thenAccept / exceptionally 等回调也可指定线程池

3. 并发场景影响对比

情况 HTTP 线程在做什么 对其他用户的影响
调用了 future.get() 被卡住,干等结果 线程池易耗尽,其他请求排队或失败
使用 CompletableFuture 链式(不 get) 提交任务后立刻释放 几乎不影响其他用户,并发能力好

三、为什么「Future 不调用 get()」不够好?

有人会想:既然 get() 会阻塞,那提交 Future 后不调用 get(),不就能立刻返回了吗?表面可以,但存在严重缺陷。

1. 异常会被默默吞掉(最严重)

Future<?> future = executor.submit(() -> {
    throw new RuntimeException("PDF生成失败");
});

// 如果不调用 get(),这个异常就彻底消失了
// 没有人知道任务失败了,日志可能都没有

CompletableFuture 可以挂上 exceptionallywhenComplete,失败了能立刻感知并处理。

2. 无法继续后续步骤

实际业务中,生成 PDF 后通常还需要:

  • 上传到 OSS
  • 把下载地址写进数据库
  • 发消息通知用户
  • 更新任务状态为「已完成」

普通 Futureget() 时,根本没有机会在任务完成后自动执行这些逻辑。

CompletableFuture 可以优雅地链式编写:

CompletableFuture.supplyAsync(() -> generatePdf(), executor)
    .thenApply(pdf -> uploadToOss(pdf))
    .thenAccept(url -> {
        updateTaskStatus(taskId, "SUCCESS", url);
        sendNotify(userId, url);
    })
    .exceptionally(ex -> {
        updateTaskStatus(taskId, "FAILED", ex.getMessage());
        return null;
    });

3. 没有完成通知机制

普通 Future 没有回调。想知道任务何时完成,只能:

  • 轮询 isDone()(浪费资源)
  • 或强制 get()(又变回阻塞)

4. 三种做法对比

做法 能否立刻返回 能否感知成功/失败 能否自动继续下一步 推荐程度
Future + get() 否(阻塞) 能(但阻塞) 不推荐(高并发)
Futureget() 不能 不能 很差(火后不管)
CompletableFuture 链式 推荐

四、线程模型澄清

常见表述「future.get() 占用主线程」需要更精确:

  • future.get() 占用的是调用它的那个线程。在 Web 项目中,这个线程通常是 HTTP 请求线程,而不是程序入口的 main 线程。
  • FutureCompletableFuture 都可以把任务本身放到自定义线程池中执行。
  • 关键差异在于:等待结果时,谁被卡住get() 会卡住调用线程;CompletableFuture 的回调可以在指定线程池中执行,调用线程不受影响。
对比点 Future + get() CompletableFuture
任务执行线程 可用自定义线程池 可用自定义线程池
等待结果时谁被卡住 调用 get() 的线程(通常是 HTTP 线程) 不会卡住调用线程,回调在指定线程池执行
高并发场景是否推荐 不推荐 推荐

五、总结与建议

  1. Future 只是「结果容器」,CompletableFuture 是「异步流程引擎」。
  2. 在需要组合多个异步操作、非阻塞回调、优雅处理异常的场景下,应优先使用 CompletableFuture
  3. 简单「提交任务 + 不 get」属于残缺的异步:无法感知失败,也无法自动继续后续逻辑。
  4. 高并发 Web 服务中,避免在 HTTP 线程上调用 get(),否则会显著降低系统吞吐。
  5. 实际项目(如异步 PDF 报告生成)推荐:CompletableFuture + 独立 ThreadPoolTaskExecutor + 超时控制 + 异常兜底。

适用场景提示:PDF/报表生成、多接口聚合、文件处理、消息通知等耗时操作,均适合用 CompletableFuture 做异步编排。