主线程如何得知线程池中多个并行任务的完成并收集它们的结果?
主线程等待多个并行任务完成并收集结果,常见有三种方式:
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 对象本身保证的,跟执行顺序无关。