Java JUC:CompletableFuture 详解,多个任务并行执行并等待全部完成

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 里用来做异步任务编排的工具,特别适合处理多个互不依赖的耗时任务。

相关推荐
学习3人组1 小时前
Python 评论朴素贝叶斯文本情感分析示例
人工智能·python·机器学习
JAVA面经实录9171 小时前
MongoDB(文档型 NoSQL)
java·数据库·mongodb·nosql
cfm_29141 小时前
JVM类加载机制初步了解
java·jvm
让我上个超影吧1 小时前
Cluade code:上下文压缩
java·服务器·ai
2401_885665191 小时前
从零搭建卷积神经网络:基于PyTorch实现MNIST手写数字分类
pytorch·python·神经网络·算法·机器学习·分类·cnn
plainGeekDev1 小时前
批量写入 → Room 事务
android·java·kotlin
宋哥转AI1 小时前
MCP 第一天我没写@Tool,先在一个大仓库里划这三层
java·agent·mcp
填满你的记忆1 小时前
MCP协议是什么?为什么它被称为AI时代的“USB接口”?
java·人工智能·agent·mcp
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【72】集成 MCP 客户端
java·人工智能·spring