接口响应慢到崩溃?CompletableFuture 并行编排让效率提升 3 倍

前言

在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);
  • CompletableFutureget()方法是阻塞的,尽量避免使用。如果必须要使用的话,需要添加超时时间,否则可能会导致主线程一直等待,无法执行其他任务。

实战代码演示:

下面我会围绕电商、数据查询、接口聚合等高频业务场景,给出可直接运行的代码示例,并解释每个场景的核心价值。

场景 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);
    }
}
  • thenComposethenApply的区别:thenApply接收同步函数,thenCompose接收返回 Future 的异步函数,适合多步异步依赖。
  • 整个流程异步执行,主线程无需等待第一步完成再执行第二步,充分利用线程资源。
  • thenApply/thenCompose实现的异步串行依赖,不会缩短单个请求的任务总耗时 ,但能解放主线程,提升系统整体的并发响应能力;耗时的串行依赖任务 → 用thenCompose保证真异步,避免阻塞前序任务线程;

对比

当多个任务的执行不需要依赖彼此的结果,每个任务的输入都是独立的(比如仅依赖初始的入参,而非其他任务的输出),执行顺序不影响最终结果。能直接缩短总耗时。

其他的需要依赖彼此结果的,比如要先查到用户id,才能去查用户的订单详情。这种情况不能直接缩短耗时,但能提高并发量。因为是异步执行的,主线程不会阻塞。

总结:

CompletableFuture为Java提供了强大的异步编程能力,可以极大地提高应用的并发能力和响应速度。通过并行执行多个查询任务,我们可以大幅减少接口的响应时间,优化用户体验。同时,CompletableFuture的代码风格函数式、简洁、优雅,也使得代码更加易读易维护。

但是,异步编程也不是万能的,它需要开发者转变思维模式,还需要权衡利弊。在实际项目中,我们可以结合其他优化手段,选择合适的方案,以达到最佳的性能效果。

相关推荐
程序员cxuan2 小时前
GPT-5.6 还不发布?不过大家可以先看看 Codex 的白皮书。
人工智能·后端·程序员
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(八):设计管理员模型、热重载配置
前端·后端·go
ServBay2 小时前
拒绝当二等公民,Windows 开发者如何无痛开启 Claude Code 本地全栈运维?
后端·ai编程·mcp
用户34232323763172 小时前
从数据源到仪表盘——全链路端到端实战整合
后端
Apifox2 小时前
从 Postman 迁移到 Apifox:Workspace、Collection、Environment 现在可以一起导入了
前端·后端·程序员
用户7713970207064 小时前
深入解析 C# Path.ChangeExtension:原来改扩展名可以这么简单
后端
zimoyin4 小时前
深入理解 Kotlin 协程:从零实现一个 IO 优先 + 虚拟线程溢出的混合调度器
后端
雨落倾城夏未凉4 小时前
第四章c#方法-参数数组和可选参数(16)
后端·c#