前言
在Web应用开发中,一个界面可能需要同时请求多个接口来获取不同信息。传统的做法是编写一个聚合接口同步获取这些数据,第二种方法是分多次请求来获取数据。这两种方式虽然简单直观,但效率比较低下,随着应用复杂度的增加,这种低效的做法将会带来严重的性能问题。
异步编程模型可以很好地解决这个问题。多个任务可以同时执行,互不影响,从而大幅提高应用的响应速度和吞吐量。Java 8 中引入的CompletableFuture为异步编程提供了强有力的支持,使得编写异步代码变得更加简单。本文将重点介绍如何利用CompletableFuture优化并发查询接口的响应速度。
实现思路:
要优化并发查询接口的响应速度,传统的优化方式是通过多线程来并行执行多个查询任务。但这种做法存在一些缺陷:1.创建和管理线程的开销较大,如果线程数量过多,会给系统带来很大的压力。
2.如果查询任务的执行时间不均匀,会导致部分线程需要长时间等待,资源利用率低下。
而CompletableFuture提供了一种更优雅、更高效的解决方案。其核心思路是:
每个查询任务都封装为一个CompletableFuture异步任务,由线程池并行执行。
通过CompletableFuture.allOf()方法等待所有异步任务完成。
最后从每个任务的结果中组装出最终需要的数据对象。
一:创建CompetableFuture
ini
// 从一个供给函数创建
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");
// 从一个运行函数创建
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> System.out.println("Hello"));
// 从一个已有的结果创建
CompletableFuture<String> future = CompletableFuture.completedFuture("Hello");
二.链式调用
rust
CompletableFuture<String> resultFuture = CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World") // 对结果进行转换
.thenCompose(s -> getResult(s)); // 组合另一个异步操作
三. 异常处理
arduino
CompletableFuture<String> future
= CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("Computation error!");
}
return "hello!";
}).exceptionally(ex -> {
System.out.println(ex.toString());// CompletionException
return "world!";
});
assertEquals("world!", future.get());
四。组合多个completablefuture的结果
scss
// 等待所有任务完成
CompletableFuture.allOf(future1, future2, future3).get();
CompletableFuture.allOf(future1, future2, future3).join();
// 只要任意一个任务完成即可
CompletableFuture.anyOf(future1, future2, future3).get();
CompletableFuture.anyOf(future1, future2, future3).join();
// 规定超时时间,防止一直堵塞
CompletableFuture.allOf(future1, future2, future3).get(6, TimeUnit.SECONDS);
五.设置超时时间
ini
String result = CompletableFuture.supplyAsync(() -> "Hello")
.completeOnTimeout("Timeout!", 1, TimeUnit.SECONDS)
.get();
- 我们上面的代码示例中,为了方便,都没有选择自定义线程池。实际项目中,这是不可取的。
CompletableFuture 默认使用全局共享的 ForkJoinPool.commonPool() 作为执行器,所有未指定执行器的异步任务都会使用该线程池。这意味着应用程序、多个库或框架(如 Spring、第三方库)若都依赖 CompletableFuture,默认情况下它们都会共享同一个线程池。
虽然 ForkJoinPool 效率很高,但当同时提交大量任务时,可能会导致资源竞争和线程饥饿,进而影响系统性能。
为避免这些问题,建议为 CompletableFuture 提供自定义线程池。
java
private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
CompletableFuture.runAsync(() -> {
//...
}, executor);
CompletableFuture的get()方法是阻塞的,尽量避免使用。如果必须要使用的话,需要添加超时时间,否则可能会导致主线程一直等待,无法执行其他任务。
实战代码演示:
下面我会围绕电商、数据查询、接口聚合等高频业务场景,给出可直接运行的代码示例,并解释每个场景的核心价值。
场景 1:电商商品详情页 - 并行查询多维度数据
业务背景 :商品详情页需要展示商品基本信息、库存、价格、用户评价摘要等数据,这些数据分散在不同的 DAO / 服务中,若串行查询会导致接口响应慢。核心价值:用 CompletableFuture 并行执行多个查询任务,汇总结果,大幅缩短接口响应时间。
kotlin
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
// 模拟商品相关的服务
class ProductService {
// 查询商品基本信息(模拟耗时100ms)
public String getBaseInfo(Long productId) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
return "商品ID:" + productId + ",名称:小米14,分类:手机";
}
// 查询商品库存(模拟耗时80ms)
public Integer getStock(Long productId) {
try { Thread.sleep(80); } catch (InterruptedException e) {}
return 1000;
}
// 查询商品价格(模拟耗时120ms)
public Double getPrice(Long productId) {
try { Thread.sleep(120); } catch (InterruptedException e) {}
return 3999.0;
}
// 查询商品评价摘要(模拟耗时150ms)
public String getCommentSummary(Long productId) {
try { Thread.sleep(150); } catch (InterruptedException e) {}
return "好评率98%,累计评价10w+";
}
}
public class ProductDetailDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ProductService service = new ProductService();
Long productId = 1001L;
// 1. 异步并行执行多个查询任务
CompletableFuture<String> baseInfoFuture = CompletableFuture.supplyAsync(() -> service.getBaseInfo(productId));
CompletableFuture<Integer> stockFuture = CompletableFuture.supplyAsync(() -> service.getStock(productId));
CompletableFuture<Double> priceFuture = CompletableFuture.supplyAsync(() -> service.getPrice(productId));
CompletableFuture<String> commentFuture = CompletableFuture.supplyAsync(() -> service.getCommentSummary(productId));
// 2. 等待所有任务完成(总耗时≈最长的那个任务耗时,而非累加:150ms左右)
CompletableFuture.allOf(baseInfoFuture, stockFuture, priceFuture, commentFuture).join();
// 3. 获取所有结果并组装
String baseInfo = baseInfoFuture.get();
Integer stock = stockFuture.get();
Double price = priceFuture.get();
String comment = commentFuture.get();
// 4. 输出结果
System.out.println("商品详情:");
System.out.println(baseInfo);
System.out.println("库存:" + stock + "件");
System.out.println("价格:¥" + price);
System.out.println("评价:" + comment);
}
}
执行结果:
erlang
商品详情:
商品ID:1001,名称:小米14,分类:手机
库存:1000件
价格:¥3999.0
评价:好评率98%,累计评价10w+
- 串行执行总耗时:100+80+120+150=450ms;并行执行仅≈150ms,响应速度提升 3 倍。
supplyAsync默认使用 ForkJoinPool 线程池,实际要指定自定义线程池避免核心线程被占满。
场景 2:异步任务依赖编排 - 先查用户再查订单
业务背景 :需要先根据用户 ID 查询用户信息,再用用户信息中的会员等级查询该用户的专属订单(任务有依赖关系)。核心价值 :用 CompletableFuture 的thenApply/thenCompose实现异步任务的串行依赖,避免主线程阻塞
java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
// 模拟用户服务和订单服务
class UserService {
// 查询用户信息(返回用户ID+会员等级)
public User getUser(Long userId) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
return new User(userId, "张三", "VIP3");
}
}
class OrderService {
// 根据用户ID和会员等级查询专属订单
public String getVipOrders(Long userId, String vipLevel) {
try { Thread.sleep(150); } catch (InterruptedException e) {}
return "用户" + userId + "(" + vipLevel + ")的专属订单:[OD1001, OD1002, OD1003]";
}
}
// 用户实体类
class User {
private Long userId;
private String userName;
private String vipLevel;
public User(Long userId, String userName, String vipLevel) {
this.userId = userId;
this.userName = userName;
this.vipLevel = vipLevel;
}
// getter
public Long getUserId() { return userId; }
public String getVipLevel() { return vipLevel; }
}
public class DependentTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
UserService userService = new UserService();
OrderService orderService = new OrderService();
Long userId = 10086L;
// 1. 第一步:异步查询用户信息
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(userId));
// 2. 第二步:依赖用户信息,异步查询专属订单(thenCompose用于异步任务依赖)
CompletableFuture<String> orderFuture = userFuture.thenCompose(user ->
CompletableFuture.supplyAsync(() -> orderService.getVipOrders(user.getUserId(), user.getVipLevel()))
);
// 3. 获取最终结果
String result = orderFuture.get();
System.out.println(result);
}
}
thenCompose与thenApply的区别:thenApply接收同步函数,thenCompose接收返回 Future 的异步函数,适合多步异步依赖。- 整个流程异步执行,主线程无需等待第一步完成再执行第二步,充分利用线程资源。
thenApply/thenCompose实现的异步串行依赖,不会缩短单个请求的任务总耗时 ,但能解放主线程,提升系统整体的并发响应能力;耗时的串行依赖任务 → 用thenCompose保证真异步,避免阻塞前序任务线程;
对比
当多个任务的执行不需要依赖彼此的结果,每个任务的输入都是独立的(比如仅依赖初始的入参,而非其他任务的输出),执行顺序不影响最终结果。能直接缩短总耗时。
其他的需要依赖彼此结果的,比如要先查到用户id,才能去查用户的订单详情。这种情况不能直接缩短耗时,但能提高并发量。因为是异步执行的,主线程不会阻塞。
总结:
CompletableFuture为Java提供了强大的异步编程能力,可以极大地提高应用的并发能力和响应速度。通过并行执行多个查询任务,我们可以大幅减少接口的响应时间,优化用户体验。同时,CompletableFuture的代码风格函数式、简洁、优雅,也使得代码更加易读易维护。
但是,异步编程也不是万能的,它需要开发者转变思维模式,还需要权衡利弊。在实际项目中,我们可以结合其他优化手段,选择合适的方案,以达到最佳的性能效果。