主线程等待多个并行任务完成并收集结果,常见有三种方式:
1) Future 模式 提交任务时保存 Future 列表,主线程遍历调用 future.get() 阻塞获取结果,可以设置超时时间。缺点是串行等待,如果第一个任务慢,后面的任务结果拿不到。
2) CompletableFuture 用 CompletableFuture.allOf(futures).join() 等待所有任务完成,通过 thenApply 收集结果。支持异步回调和异常处理,单个任务失败不影响其他任务。这是目前最推荐的方式。
3) CountDownLatch 协作 初始化时设定计数值等于任务数,每个任务完成时调用 countDown(),主线程通过 latch.await() 阻塞等待计数归零。适合需要精确控制执行流程的场景。

Future 模式实现

ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<Integer>> futures = new ArrayList<>();

// 提交任务
for (int i = 0; i < 10; i++) {
    futures.add(executor.submit(() -> calculate()));
}

// 阻塞获取结果(带超时)
for (Future<Integer> future : futures) {
    try {
        Integer result = future.get(1, TimeUnit.SECONDS);
        processResult(result);
    } catch (TimeoutException e) {
        // 处理超时
    }
}

这种方式有个明显的问题:遍历 Future 列表是串行等待的。假设有 10 个任务,第 1 个任务需要 10 秒,第 2-10 个任务只需要 1 秒。用这种方式,主线程会卡在第一个 get() 上 10 秒,即使后面 9 个任务早就完成了也拿不到结果。
如果业务对响应时间敏感,这种方式不太合适。

CompletableFuture 实现

ExecutorService executor = Executors.newFixedThreadPool(5);
List<CompletableFuture<Integer>> futures = new ArrayList<>();

// 提交任务
for (int i = 0; i < 10; i++) {
    futures.add(CompletableFuture.supplyAsync(() -> calculate(), executor));
}

// 组合所有任务
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
    futures.toArray(new CompletableFuture[0])
);

// 收集结果
CompletableFuture<List<Integer>> allResults = allFutures.thenApply(v -> 
    futures.stream()
           .map(future -> future.join())
           .collect(Collectors.toList())
);

// 处理结果(异步/同步)
allResults.thenAccept(results -> results.forEach(this::processResult));

CompletableFuture 是 JDK 8 引入的,解决了 Future 的几个痛点。 单个任务异常不影响其他任务执行,支持流式处理和异步回调。还可以通过 thenCombine、thenCompose 实现复杂的任务依赖关系,比如任务 B 依赖任务 A 的结果。

CountDownLatch实现

ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(10);
List<Result> results = Collections.synchronizedList(new ArrayList<>());

// 提交任务
for (int i = 0; i < 10; i++) {
    executor.submit(() -> {
        try {
            Result result = calculate();
            results.add(result);
        } finally {
            latch.countDown(); // 任务完成后计数减一
        }
    });
}

// 主线程等待
latch.await(1, TimeUnit.MINUTES); // 带超时等待
processResults(results);

CountDownLatch 的计数器只能用一次,减到 0 就失效了。如果需要重复使用,可以换成 CyclicBarrier,它能 reset 重置。
这种方式适合批量任务完成后统一触发下一步操作的场景,比如 5 个线程各自加载一部分配置,全部加载完再启动服务。

三种方式对比

方式优点缺点适用场景
Future简单直接,JDK 5 就有串行等待,阻塞主线程任务数少、对延迟不敏感
CompletableFuture异步回调、链式调用、异常处理完善代码稍复杂复杂异步编排、生产环境首选
CountDownLatch精确控制同步点只能用一次、需要手动收集结果多线程启动/关闭协调

实际项目中,如果是 JDK 8+,基本都用 CompletableFuture。它的 API 设计得很好,能覆盖绝大多数异步场景,代码可读性也比 Future 回调嵌套好很多。

扩展

CompletableFuture 的 allOf 方法内部是怎么实现等待所有任务完成的?

allOf 返回一个新的 CompletableFuture,这个 Future 会在所有传入的 Future 都完成后才完成。内部实现是用一个原子计数器,每个子任务完成时计数减一,减到 0 时触发 allOf 返回的 Future 完成。用的是 CAS 无锁实现,性能比加锁好。

如果其中一个任务抛异常了,allOf 会怎么处理?其他任务还会继续执行吗?

其他任务会继续执行,不会因为一个任务失败就中断。但 allOf 返回的 Future 会变成异常完成状态,调用 join() 会抛出 CompletionException。如果想拿到部分成功的结果,可以在收集结果时用 handle 方法单独处理每个任务的异常,把失败的任务返回默认值或者跳过。

Future.get() 和 CompletableFuture.join() 有什么区别?

get() 会抛受检异常 InterruptedException 和 ExecutionException,调用方必须显式处理。join() 抛的是非受检异常 CompletionException,代码写起来更简洁,不用到处 try-catch。另外 get() 支持超时参数,join() 不支持,需要超时控制的话得用 orTimeout() 方法包装一下。

线程池里的任务执行完,主线程怎么知道结果放在哪个 Future 里?顺序会乱吗?

回答: 不会乱。提交任务时拿到的 Future 和任务是一一对应的,无论任务执行顺序如何,通过那个 Future 拿到的永远是对应任务的结果。比如第 5 个提交的任务可能第 1 个执行完,但它的结果只会出现在第 5 个 Future 里。这是 Future 对象本身保证的,跟执行顺序无关。