Java CompletableFuture异步任务编排实践:从任务执行到结果汇总
在Java并发编程中,CompletableFuture 凭借其灵活的异步任务编排能力,成为处理多任务异步场景的核心工具。本文将以一段实际业务代码为例,拆解 CompletableFuture 如何实现“异步执行任务 + 结果汇总 + 多任务等待”的完整流程,并提炼关键知识点与实践注意事项,帮助大家在项目中正确落地。
一、业务场景与核心代码
假设我们有一个批量备份任务的场景:
- 异步执行一批细粒度备份任务,返回一个
List<Boolean>结果(true表示备份成功,false表示失败); - 备份任务完成后,自动触发汇总逻辑(若结果非空且有数据,记录一条汇总日志/数据);
- 等待上述两个异步任务全部完成,且设置超时时间防止无限阻塞。
以下是实现该场景的核心代码:
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CompletableFutureBackupDemo {
// 自定义线程池(推荐:避免使用默认ForkJoinPool,防止线程资源耗尽)
private static final Executor BACKUP_EXECUTOR = Executors.newFixedThreadPool(5);
public static void main(String[] args) throws Exception {
// 1. 异步执行备份任务:返回List<Boolean>结果(supplyAsync适合有返回值的任务)
CompletableFuture<List<Boolean>> backupFuture = CompletableFuture.supplyAsync(
() -> executeBatchBackupTask(), // 实际执行备份的业务方法
BACKUP_EXECUTOR // 指定线程池,控制资源占用
);
// 2. 备份任务完成后,异步执行汇总逻辑(thenAcceptAsync消费前序任务结果)
CompletableFuture<Void> summaryFuture = backupFuture.thenAcceptAsync(backupResults -> {
// 汇总逻辑:仅当结果非空且有数据时执行
if (backupResults != null && !backupResults.isEmpty()) {
System.out.println("开始记录备份汇总数据...");
// 实际业务:统计成功/失败数量、写入数据库/日志等
long successCount = backupResults.stream().filter(Boolean::booleanValue).count();
long failCount = backupResults.size() - successCount;
System.out.printf("备份汇总:总任务数=%d,成功数=%d,失败数=%d%n",
backupResults.size(), successCount, failCount);
}
}, BACKUP_EXECUTOR);
// 3. 等待所有异步任务(backupFuture + summaryFuture)完成,设置3000秒超时
CompletableFuture.allOf(backupFuture, summaryFuture)
.get(3000, TimeUnit.SECONDS);
System.out.println("所有备份及汇总任务已完成!");
}
/**
* 模拟:执行批量备份的业务方法
* @return 备份结果列表(true=成功,false=失败)
*/
private static List<Boolean> executeBatchBackupTask() {
// 实际业务中可能是循环调用备份接口、读取文件备份等操作
System.out.println("异步执行批量备份任务...");
// 模拟3个备份任务结果(可根据实际业务动态生成)
return List.of(true, true, false);
}
}
二、核心代码拆解:CompletableFuture关键用法
上述代码的核心是 CompletableFuture 的三个核心能力:异步任务创建、结果链式消费、多任务等待,我们逐段解析:
1. 异步任务创建:supplyAsync
CompletableFuture<List<Boolean>> backupFuture = CompletableFuture.supplyAsync(
() -> executeBatchBackupTask(),
BACKUP_EXECUTOR
);
- 作用:创建一个有返回值的异步任务(若任务无返回值,用
runAsync); - 参数说明:
- 第一个参数:
Supplier<List<Boolean>>函数式接口,封装实际业务逻辑(这里是executeBatchBackupTask批量备份); - 第二个参数:
Executor自定义线程池(强烈推荐),避免使用默认的ForkJoinPool.commonPool()(默认线程数与CPU核心数相关,高并发下易耗尽);
- 第一个参数:
- 返回值:
CompletableFuture<List<Boolean>>,代表异步任务的“未来结果”,后续可基于它编排后续任务。
2. 结果链式消费:thenAcceptAsync
CompletableFuture<Void> summaryFuture = backupFuture.thenAcceptAsync(backupResults -> {
if (backupResults != null && !backupResults.isEmpty()) {
// 汇总逻辑
}
}, BACKUP_EXECUTOR);
- 作用:作为
backupFuture的“回调任务”,在backupFuture完成后异步消费其结果(无返回值,若需返回新结果用thenApplyAsync); - 关键特性:
- 顺序性:
summaryFuture会严格等待backupFuture完成后才执行,避免手动加锁或等待; - 异步性:通过第二个
Executor参数指定线程池,若不指定则默认使用前序任务的线程池(或ForkJoinPool);
- 顺序性:
- 结果安全:代码中先判断
backupResults != null,再调用isEmpty(),避免空指针异常(异步任务可能返回null,需严谨判空)。
3. 多任务等待:allOf + get(timeout)
CompletableFuture.allOf(backupFuture, summaryFuture)
.get(3000, TimeUnit.SECONDS);
allOf作用:将多个CompletableFuture合并为一个新的CompletableFuture<Void>,仅当所有子任务都完成时,合并后的任务才会完成;- 注意:
allOf不返回子任务的结果,仅表示“所有任务完成”的状态;若需获取多个子任务结果,需单独调用backupFuture.get()(但需确保已完成,或通过join()非阻塞获取)。
- 注意:
get(timeout)作用:阻塞当前线程,等待合并后的任务完成,同时设置超时时间(这里是3000秒);- 若超时未完成,会抛出
TimeoutException,避免线程无限阻塞; - 若子任务执行中抛出异常,
get()会抛出ExecutionException,需通过异常处理逻辑捕获。
- 若超时未完成,会抛出
三、实践注意事项(避坑指南)
在使用 CompletableFuture 时,以下几点直接影响代码的稳定性与性能,必须重点关注:
1. 务必使用自定义线程池,拒绝默认线程池
CompletableFuture 的默认线程池是 ForkJoinPool.commonPool(),其线程数由 java.util.concurrent.ForkJoinPool.common.parallelism 控制(默认值为 CPU核心数 - 1)。
问题:若项目中大量使用默认线程池,高并发下会导致线程资源被耗尽,所有异步任务阻塞。
解决方案:像示例中那样,创建自定义线程池(如 FixedThreadPool、ThreadPoolExecutor),并根据业务压力调整核心线程数、最大线程数、队列容量。
2. 必须处理异步任务的异常
示例代码未展示异常处理,若 executeBatchBackupTask 抛出异常(如数据库连接失败、IO异常),会导致 backupFuture 进入异常状态,后续 summaryFuture 不会执行,且 get() 会抛出 ExecutionException。
常用异常处理方式:
- 方式1:用
exceptionally捕获异常并返回默认值:CompletableFuture<List<Boolean>> backupFuture = CompletableFuture.supplyAsync( () -> executeBatchBackupTask(), BACKUP_EXECUTOR ).exceptionally(ex -> { // 捕获异常,打印日志并返回默认空列表 System.err.println("备份任务执行失败:" + ex.getMessage()); return List.of(); // 返回默认值,避免后续任务因空指针崩溃 }); - 方式2:用
handle同时处理正常结果与异常:CompletableFuture<List<Boolean>> backupFuture = CompletableFuture.supplyAsync( () -> executeBatchBackupTask(), BACKUP_EXECUTOR ).handle((result, ex) -> { if (ex != null) { System.err.println("备份任务失败:" + ex.getMessage()); return List.of(); } return result; // 正常结果直接返回 });
3. 合理设置超时时间
示例中 get(3000, TimeUnit.SECONDS) 超时时间为3000秒(50分钟),实际业务中需根据任务耗时调整(如备份任务通常几分钟内完成,可设为 300 秒)。
超时的意义:防止因下游服务挂死、网络阻塞等问题,导致当前线程永久阻塞,占用资源。
4. 避免“线程泄漏”
若自定义线程池使用 Executors.newFixedThreadPool() 等无界队列的线程池,当任务积压过多时,会导致内存溢出。
优化方案:使用 ThreadPoolExecutor 手动配置,设置有界队列与拒绝策略:
private static final Executor BACKUP_EXECUTOR = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列(容量100)
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:任务满时抛出异常,及时发现问题
);
四、完整可运行代码
将上述优化点(异常处理、合理线程池)整合,给出完整可运行代码:
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class CompletableFutureBackupBestPractice {
// 自定义线程池:有界队列+拒绝策略,避免资源耗尽
private static final Executor BACKUP_EXECUTOR = new ThreadPoolExecutor(
5,
10,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) {
try {
// 1. 异步执行备份任务(带异常处理)
CompletableFuture<List<Boolean>> backupFuture = CompletableFuture.supplyAsync(
() -> executeBatchBackupTask(), BACKUP_EXECUTOR
).exceptionally(ex -> {
System.err.println("备份任务执行异常:" + ex.getMessage());
ex.printStackTrace();
return List.of(); // 返回空列表,确保后续汇总任务可正常执行
});
// 2. 异步汇总结果(带异常处理)
CompletableFuture<Void> summaryFuture = backupFuture.thenAcceptAsync(backupResults -> {
try {
if (backupResults != null && !backupResults.isEmpty()) {
long success = backupResults.stream().filter(Boolean::booleanValue).count();
long fail = backupResults.size() - success;
String summary = String.format(
"备份汇总【%s】:总任务数=%d,成功=%d,失败=%d",
LocalDateTime.now(), backupResults.size(), success, fail
);
System.out.println(summary);
// 实际业务:将汇总结果写入数据库
// saveSummaryToDB(summary);
} else {
System.out.println("备份结果为空,无需汇总");
}
} catch (Exception e) {
System.err.println("汇总任务执行异常:" + e.getMessage());
e.printStackTrace();
}
}, BACKUP_EXECUTOR);
// 3. 等待所有任务完成,超时时间设为300秒(5分钟)
CompletableFuture.allOf(backupFuture, summaryFuture)
.get(300, TimeUnit.SECONDS);
System.out.println("所有任务执行完成,程序退出");
} catch (TimeoutException e) {
System.err.println("任务执行超时,强制终止");
} catch (InterruptedException e) {
System.err.println("线程被中断:" + e.getMessage());
Thread.currentThread().interrupt(); // 恢复中断状态
} catch (Exception e) {
System.err.println("主线程异常:" + e.getMessage());
e.printStackTrace();
} finally {
// 关闭线程池(若为长期运行服务,无需关闭;若为批处理程序,需关闭释放资源)
if (BACKUP_EXECUTOR instanceof ThreadPoolExecutor) {
((ThreadPoolExecutor) BACKUP_EXECUTOR).shutdown();
}
}
}
/**
* 模拟批量备份任务:随机生成成功/失败结果
*/
private static List<Boolean> executeBatchBackupTask() {
System.out.println("开始执行批量备份任务...");
// 模拟任务耗时(1-3秒)
try {
TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextInt(1, 4));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("备份任务被中断", e);
}
// 模拟10个备份任务的结果(随机true/false)
return ThreadLocalRandom.current().ints(10, 0, 2)
.mapToObj(i -> i == 1)
.collect(Collectors.toList());
}
}
五、总结
CompletableFuture 相比传统的 Thread、Future,最大优势在于无需手动管理线程生命周期,且支持链式调用、多任务合并、超时控制,极大简化了异步任务编排。
核心要点回顾:
- 用
supplyAsync(有返回值)/runAsync(无返回值)创建异步任务,必用自定义线程池; - 用
thenAcceptAsync(消费结果)/thenApplyAsync(转换结果)实现链式任务; - 用
allOf合并多任务,用get(timeout)控制超时; - 必须处理异常(
exceptionally/handle),避免任务静默失败; - 线程池需合理配置(有界队列、拒绝策略),防止资源泄漏。
掌握这些用法,即可应对大多数异步业务场景(如批量接口调用、数据同步、多服务联调等),提升系统的并发能力与稳定性。
953

被折叠的 条评论
为什么被折叠?



