Java JUC:CompletableFuture 详解,多个任务并行执行并等待全部完成
在学习 Java JUC 的时候,经常会遇到一个类:CompletableFuture。
一开始我可能会把它记成 featurecomplete,但它真正的名字是:
java
CompletableFuture
它是 Java 8 引入的异步编程工具,属于 java.util.concurrent 包,也就是 JUC 体系中的一部分。
它最常见的作用就是:
开启多个异步任务,让它们并行执行,然后等待全部执行完成,最后统一获取结果。
这在后端开发中非常常见。
比如一个订单详情接口,需要查询:
text
用户信息
订单信息
优惠券信息
物流信息
如果这几个查询之间没有强依赖关系,就可以同时执行,而不是一个一个查。
一、为什么需要 CompletableFuture?
假设现在有三个任务:
java
queryUser();
queryOrder();
queryCoupon();
如果我们直接这样写:
java
queryUser();
queryOrder();
queryCoupon();
它们就是串行执行。
执行过程类似这样:
text
查询用户信息 -> 查询订单信息 -> 查询优惠券信息
如果每个任务耗时 1 秒,总耗时大概就是 3 秒。
但是如果这三个任务之间没有依赖关系,就可以让它们同时执行:
text
查询用户信息 ┐
查询订单信息 ├── 同时执行
查询优惠券信息 ┘
这样总耗时可能只需要 1 秒左右。
这就是 CompletableFuture 的价值。
二、CompletableFuture 是什么?
CompletableFuture 可以理解成:
一个代表未来结果的对象。
你现在开启了一个任务,但这个任务可能还没有执行完。
于是 Java 先给你一个 CompletableFuture 对象。
等任务执行完以后,你可以通过这个对象拿到结果。
比如:
java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "用户信息";
});
String result = future.join();
System.out.println(result);
这里的 future 就代表一个未来才会完成的任务结果。
三、runAsync 和 supplyAsync 的区别
创建异步任务常用两个方法:
java
CompletableFuture.runAsync()
CompletableFuture.supplyAsync()
它们的区别很简单。
1. runAsync:没有返回值
如果任务不需要返回结果,可以用 runAsync。
java
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("执行异步任务:" + Thread.currentThread().getName());
});
future.join();
适合这种场景:
text
发送短信
写日志
发送通知
异步刷新缓存
因为它只是执行一个动作,不关心返回值。
2. supplyAsync:有返回值
如果任务需要返回结果,就用 supplyAsync。
java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "用户信息";
});
String result = future.join();
System.out.println(result);
适合这种场景:
text
查询用户信息
查询订单信息
调用远程接口
计算某个结果
所以可以简单记:
text
runAsync:只执行,不返回结果
supplyAsync:执行完以后返回结果
四、join 是什么?
join() 的作用是:
等待异步任务执行完成,并获取结果。
例如:
java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "hello";
});
String result = future.join();
System.out.println(result);
如果异步任务还没有完成,join() 会阻塞等待。
如果任务已经完成,join() 会直接拿到结果。
五、多个任务并行执行,然后等待全部完成
这是 CompletableFuture 最常见的用法。
假设现在有三个任务:
text
查询用户信息
查询订单信息
查询优惠券信息
代码可以这样写:
java
CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "用户信息";
});
CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "订单信息";
});
CompletableFuture<String> couponFuture = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "优惠券信息";
});
CompletableFuture.allOf(userFuture, orderFuture, couponFuture).join();
String user = userFuture.join();
String order = orderFuture.join();
String coupon = couponFuture.join();
System.out.println(user);
System.out.println(order);
System.out.println(coupon);
辅助方法:
java
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
核心代码是这一句:
java
CompletableFuture.allOf(userFuture, orderFuture, couponFuture).join();
它的意思是:
等待 userFuture、orderFuture、couponFuture 全部执行完成。
六、为什么 allOf 之后还要分别 join?
可能会有一个疑问:
既然已经写了:
java
CompletableFuture.allOf(userFuture, orderFuture, couponFuture).join();
为什么后面还要写:
java
String user = userFuture.join();
String order = orderFuture.join();
String coupon = couponFuture.join();
原因是:
allOf()只负责等待所有任务完成,不负责帮你返回每个任务的结果。
也就是说,allOf() 的主要作用是"等"。
真正要拿结果,还得从每一个 CompletableFuture 里面取。
完整流程是:
text
1. 创建多个 CompletableFuture 任务
2. 这些任务并行执行
3. allOf 等待它们全部完成
4. 分别 join 获取每个任务的返回值
七、后端开发中的实际例子
比如现在有一个订单详情接口,需要返回用户信息、订单信息、优惠券信息。
以前可能会这样写:
java
@GetMapping("/detail")
public OrderDetailVO detail(Long orderId) {
User user = userService.getUserByOrderId(orderId);
Order order = orderService.getOrderById(orderId);
List<Coupon> coupons = couponService.getCouponsByOrderId(orderId);
OrderDetailVO vo = new OrderDetailVO();
vo.setUser(user);
vo.setOrder(order);
vo.setCoupons(coupons);
return vo;
}
这种写法是串行执行。
执行过程是:
text
先查用户
再查订单
再查优惠券
如果每个查询耗时 1 秒,总共大概需要 3 秒。
可以改成 CompletableFuture 并行执行:
java
@GetMapping("/detail")
public OrderDetailVO detail(Long orderId) {
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> {
return userService.getUserByOrderId(orderId);
});
CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() -> {
return orderService.getOrderById(orderId);
});
CompletableFuture<List<Coupon>> couponFuture = CompletableFuture.supplyAsync(() -> {
return couponService.getCouponsByOrderId(orderId);
});
CompletableFuture.allOf(userFuture, orderFuture, couponFuture).join();
User user = userFuture.join();
Order order = orderFuture.join();
List<Coupon> coupons = couponFuture.join();
OrderDetailVO vo = new OrderDetailVO();
vo.setUser(user);
vo.setOrder(order);
vo.setCoupons(coupons);
return vo;
}
这样三个查询可以同时执行。
执行过程变成:
text
查询用户信息 ┐
查询订单信息 ├── 同时执行
查询优惠券信息 ┘
如果每个查询都耗时 1 秒,那么总耗时可能接近 1 秒,而不是 3 秒。
八、真实项目中建议指定线程池
前面的写法是:
java
CompletableFuture.supplyAsync(() -> {
return "结果";
});
如果不指定线程池,默认使用的是:
java
ForkJoinPool.commonPool()
但是在真实 Spring Boot 项目里,一般不推荐直接使用默认线程池。
更推荐自己定义线程池。
例如:
java
ExecutorService executor = Executors.newFixedThreadPool(10);
然后把线程池传给 supplyAsync:
java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "任务结果";
}, executor);
完整例子:
java
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
return "用户信息";
}, executor);
CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> {
return "订单信息";
}, executor);
CompletableFuture<String> couponFuture = CompletableFuture.supplyAsync(() -> {
return "优惠券信息";
}, executor);
CompletableFuture.allOf(userFuture, orderFuture, couponFuture).join();
String user = userFuture.join();
String order = orderFuture.join();
String coupon = couponFuture.join();
System.out.println(user);
System.out.println(order);
System.out.println(coupon);
这样做的好处是:
text
线程数量可控
方便监控和排查问题
避免大量任务挤占公共线程池
九、join 和 get 的区别
获取异步任务结果有两个常见方法:
java
future.join();
future.get();
它们都可以等待任务完成并获取结果。
区别主要在异常处理上。
1. get 需要处理受检异常
java
try {
String result = future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
get() 会强制你处理异常。
2. join 不强制处理异常
java
String result = future.join();
join() 不需要显式写 try-catch。
所以在很多业务代码中,大家更常用 join()。
不过要注意,join() 并不是不会抛异常,而是会把异常包装成运行时异常抛出来。
十、异常处理:exceptionally
如果异步任务执行过程中报错,可以使用 exceptionally 做兜底处理。
例如:
java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
int i = 1 / 0;
return "成功";
}).exceptionally(e -> {
System.out.println("任务异常:" + e.getMessage());
return "默认值";
});
String result = future.join();
System.out.println(result);
输出结果类似:
text
任务异常:java.lang.ArithmeticException: / by zero
默认值
也就是说:
text
如果任务正常执行,就返回正常结果
如果任务执行异常,就返回默认值
十一、thenApply:上一个任务完成后继续处理
thenApply 表示:
上一步有返回值,拿到返回值以后继续处理,并返回一个新的结果。
例如:
java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "hello";
}).thenApply(result -> {
return result + " world";
});
System.out.println(future.join());
输出:
text
hello world
执行过程是:
text
第一个任务返回 hello
thenApply 拿到 hello
处理成 hello world
十二、thenAccept:只消费结果,不返回新结果
thenAccept 表示:
拿到上一步的结果,消费一下,但是不返回新的结果。
例如:
java
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
return "用户信息";
}).thenAccept(result -> {
System.out.println("拿到结果:" + result);
});
future.join();
这里 thenAccept 只是打印结果,不再返回新的数据。
十三、anyOf:谁先完成就用谁
除了 allOf,还有一个常见方法叫 anyOf。
allOf 是等待全部任务完成。
java
CompletableFuture.allOf(future1, future2, future3)
anyOf 是只要有一个任务完成就继续。
java
CompletableFuture.anyOf(future1, future2, future3)
例如:
java
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
sleep(3000);
return "接口1结果";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "接口2结果";
});
Object result = CompletableFuture.anyOf(future1, future2).join();
System.out.println(result);
输出结果一般是:
text
接口2结果
因为 future2 只睡了 1 秒,更快返回。
anyOf 适合这种场景:
text
多个接口都能获取同一份数据,谁先返回就用谁
十四、常用方法总结
| 方法 | 作用 |
|---|---|
runAsync() |
异步执行任务,没有返回值 |
supplyAsync() |
异步执行任务,有返回值 |
allOf() |
等待多个任务全部完成 |
anyOf() |
等待任意一个任务完成 |
join() |
阻塞等待并获取结果 |
get() |
阻塞等待并获取结果,需要处理受检异常 |
thenApply() |
拿到上一步结果,继续处理,并返回新结果 |
thenAccept() |
拿到上一步结果,只消费,不返回结果 |
thenRun() |
上一步完成后执行,不关心上一步结果 |
exceptionally() |
异常兜底处理 |
十五、最常用模板
日常开发中,如果只是想实现:
text
多个任务并行执行
等待全部完成
最后统一获取结果
可以直接记住这个模板:
java
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
return "任务1结果";
}, executor);
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
return "任务2结果";
}, executor);
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
return "任务3结果";
}, executor);
CompletableFuture.allOf(future1, future2, future3).join();
String result1 = future1.join();
String result2 = future2.join();
String result3 = future3.join();
System.out.println(result1);
System.out.println(result2);
System.out.println(result3);
这段代码可以理解成:
text
future1、future2、future3 同时执行
allOf 等它们全部执行完
最后分别 join 获取每个任务的结果
十六、使用 CompletableFuture 时要注意什么?
虽然 CompletableFuture 很好用,但也不能乱用。
需要注意几个问题。
1. 不是所有任务都适合并行
如果任务之间有前后依赖关系,就不能简单并行。
比如:
text
先查用户
再根据用户 ID 查订单
再根据订单 ID 查物流
这种任务有依赖关系,不能直接全部同时查。
但是如果是:
text
查用户信息
查订单信息
查优惠券信息
它们互不依赖,就很适合并行。
2. 不要无限制创建异步任务
如果一个接口里创建大量异步任务,可能会导致线程池被打满。
所以真实项目中一定要控制线程池大小。
3. 异步不一定更快
如果任务本身很轻,比如只是简单加减乘除,用异步反而可能更慢。
因为异步任务本身也有线程调度成本。
CompletableFuture 更适合这种任务:
text
数据库查询
远程接口调用
文件读取
耗时计算
十七、总结
CompletableFuture 是 Java JUC 中非常重要的异步编程工具。
它最核心的作用就是:
让多个任务并行执行,然后等待全部完成,最后统一获取结果。
刚开始学习时,重点掌握这三个方法就够了:
java
CompletableFuture.supplyAsync()
CompletableFuture.allOf()
future.join()
它们组合起来就是:
text
创建异步任务
并行执行任务
等待所有任务完成
获取每个任务结果
一句话总结:
CompletableFuture 就是 Java 里用来做异步任务编排的工具,特别适合处理多个互不依赖的耗时任务。