【Java并发编程】使用CompletableFuture最佳实践

news/2025/3/14 18:21:56/

文章目录

  • 1. 什么是CompletableFuture
  • 2. 为什么需要CompletableFuture
  • 3. 使用CompletableFuture
    • 创建类
    • 接续类(thenXxx)
  • 4. 使用CompletableFuture的一般范式

CompletableFuture是Future的增强版,是多线程开发的利器。本文通俗易懂的介绍了CompletableFuture的用法,最后祭出CompletableFuture的一般使用范式,开箱即用,质量可靠。

1. 什么是CompletableFuture

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {}
  • Future:代表异步计算的结果
  • CompletionStage:代表异步计算的一个阶段,代表了复杂计算

从以上定义可以看出,CompletableFuture不仅实现了Future接口,还实现了CompletionStage接口,是对Future功能的增强。从Java 8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。

2. 为什么需要CompletableFuture

使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。

当需要异步回调、需要复杂计算的支持的时候,CompletableFuture也能大显身手。

3. 使用CompletableFuture

CompletableFuture类的方法非常多,单纯记忆很麻烦,我们需要对它进行分类

创建类

  • supplyAsync:异步执行,有返回值

  • runAsync:异步执行,无返回值

  • anyOf:任意一个执行完成,就可以进行下一步动作

  • allOf:全部完成所有任务,才可以进行下一步任务

// 返回一个0
CompletableFuture.supplyAsync(() -> 0);

接续类(thenXxx)

  • 接续类是CompletableFuture最重要的特性,没有这个的话,CompletableFuture就没意义了,用于注入回调行为。

  • 我们知道Java 8函数式接口有4种常见类型Function、Supplier、Consumer、Runnable,好巧不巧的是CompletableFuture的续传方法也支持这4种类型的接口,一一对应关系列出如下表:

Java 8函数式接口CompletableFuture的接续方法说明
Function(有参数有返回)thenApply方法不使用thenApplyAsync异步
Supplier(无参数有返回)supplyAsync方法,runAsync方法在创建CompletableFuture时使用过了
Consumer(有参数无返回)thenAccept方法不使用thenAcceptAsync异步
Runnable(无参数无返回)thenRun方法不使用thenRunAsync异步
  • 使用thenXxx方法即可,必要使用Async后缀的异步方法。因为后一个步骤依赖前一个步骤的结果
// 以异步计算1+2+3为例
CompletableFuture.supplyAsync(() -> {return 0;
}).thenApply(v -> {try { TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace(); }return v + 1;
}).thenApply(v -> {try { TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace(); }return v + 2;
}).thenApply(v -> {try { TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace(); }return v + 3;
});
System.out.println("main线程可以异步干点别的事情,不用等待计算完成");

4. 使用CompletableFuture的一般范式

一般的,我们使用CompletableFuture就是为了使用多线程异步加快查询速度的,更多的读操作而不是写操作,所以就有必要创建多个CompletableFuture对象。

假设,在微服务架构中,需要一次性返回用户端的订单、收货地址,而这些信息分别在订单微服务、收货地址微服务中,为了快速响应用户,必然是选择开启多个线程同时查询2个微服务最后合并查询结果返回给用户。

一般的,使用以下CompletableFuture的一般范式就可以,复制粘贴开箱即用。

Result result = new Result();
// <1> 查询订单
CompletableFuture<List<Order>> orderCompletableFuture = CompletableFuture.supplyAsync(() -> {// <1.1> http查询订单List<Order> orderList = queryOrderList(uid);// <1.2> 设置结果集result.setOrderList(orderList);return orderList;
})
// <2> 后续处理
.thenApply(v -> {System.out.println("此处可以继续处理...");return v;
});CompletableFuture<List<Address>> addressCompletableFuture = CompletableFuture.supplyAsync(() -> {List<Address> addressList = queryAddressList(uid);result.setAddressList(addressList);return addressList;
});// <3> 等待所有任务执行完成
CompletableFuture.allOf(orderCompletableFuture, addressCompletableFuture);// <4> 记录异常信息并抛出
List<String> errMessage = new ArrayList<>();
List<CompletableFuture<?>> completableFutures = Arrays.asList(whenComplete, whenComplete1);
for (int i = 0; i < completableFutures.size(); i++) {CompletableFuture<?> future = completableFutures.get(i);int finalI = i;future.exceptionally(ex -> {String str = "列表位置索引" + finalI + "处发生异常,异常信息是" + ex.getMessage();errMessage.add(str);throw new RuntimeException("Error occurred: " + ex);});
}// <5> 异常处理
if(CollUtil.isNotEmpty(errMessage)){log.error("计算过程发生异常,异常有{}处,异常详细信息是:{}", errMessage.size(), errMessage);throw new RuntimeException("存在异常,可能有多处请逐个排查,异常信息列表是" + errMessage);
}
// <6> 返回结果给用户
return result;
  • <1>处,使用了CompletableFuture.supplyAsync方法,异步且有返回,这是最常用的方式

    supplyAsync方法是异步有返回值,而runAsync是异步没有返回值,supplyAsync方法更加通用,选择它就完事了

    • <1.1>处,可以通过RestTemplate或Ribbon等等方式从订单微服务查询订单并解析
    • <1.2>处,设置订单信息到结果集中
  • <2>处,可以对上一步的信息做进一步处理。可选的。

    此处可以使用thenApply、thenAccept、thenRun等方法

    因为依赖上一步的结果,所以建议这里都使用thenXxx方法而没有必要使用thenXxxAsync异步方法

  • <3>处,等待订单查询和地址查询都完成

  • <4>处,记录异常日志并重新抛出异常。必要的。

    必须记录日志,如果不记录异常,会导致异常信息丢失

  • <5>处,是否重新抛出异常。可选的。

    有的情况下,个别接口异常我们也是可以接受的,可以不抛出异常,但是必须记录异常。

  • <6>处,给用户返回订单和地址的结果集


http://www.ppmy.cn/news/1016172.html

相关文章

Python 调用自定义函数

新手入坑。 通常我们需要把公共函数提出来&#xff0c;作为公共资源调用。也避免了代码的重复书写。 比如我们在项目内创建我们的py脚本路径如下&#xff1a; 在公共方法中定义方法&#xff1a; class CommonMethods:def dataFormat(df):dataList []for row in range(0, df.…

flutter开发实战-video_player视频播放功能及视频缓存

flutter开发实战-video_player视频播放功能及视频缓存 最近开发过程中video_player播放视频&#xff0c; 一、引入video_player 在pubspec.yaml引入video_player video_player: ^2.7.0在iOS上&#xff0c;video_player使用的是AVPlayer进行播放。 在Android上&#xff0c;…

学习Pull request

我从我的导师Xing Fan指导和帮助&#xff0c;利用我的导师chunlong Li提供ChatGPT&#xff0c;在百度搜索&#xff0c;学习一些资料。以下很多内容都是我的导师Xing Fan做的。谢谢Xing Fan。考虑到隐私&#xff0c;不适合截图公开。 第一步&#xff1a; 打开Git Bash Here 如…

linux安装C++ opencv

1. 安装依赖 opencv中的一些图像、视频相关的功能需要一些依赖&#xff0c;因此在安装opencv之前需要先安装这些依赖&#xff1b;在使用apt安装相关依赖时&#xff0c;会出现无法安装的情况&#xff0c;这时可以用aptitude来降级安装。 名称apt package 名称功能编译系统buil…

ffmpeg源码编译成功,但是引用生成的静态库(.a)报错,报错位置在xxx_list.c,报错信息为某变量未定义

背景&#xff1a;本文是对上一个文章的补充&#xff0c;在源码编译之前&#xff0c;项目是有完整的ffmpeg编译脚本的&#xff0c;只不过新增了断点调试ffmpeg&#xff0c;所以产生的上面的文章&#xff0c;也就是说&#xff0c;我在用make编译成功后&#xff0c;再去做的源码编…

代码分析Java中的BIO与NIO

开发环境 OS&#xff1a;Win10&#xff08;需要开启telnet服务&#xff0c;或使用第三方远程工具&#xff09; Java版本&#xff1a;8 BIO 概念 BIO(Block IO)&#xff0c;即同步阻塞IO&#xff0c;特点为当客户端发起请求后&#xff0c;在服务端未处理完该请求之前&#xff…

CC++内存管理与模版初阶

目录 四、C&C内存管理 (一)C/C内存分布 (二)C内存管理方式 1、new/delete操作内置类型 2、new和delete操作自定义类型 (三)operator new与operator delete函数 (四)new和delete的实现原理 1、内置类型 2、自定义类型 (五)定位new表达式(placement-new) (六)八股文 1、n…

Vue3 动态列 <el-table-column> 实现 formatter 的两种方法

文章目录 动态列实现动态列实现formatter第一种第二种方法 动态列实现 参考此篇文章 Vue3 动态列实现 动态列实现formatter 第一种 以此为例&#xff1a;传递该行的wxUserInfo字段&#xff08;对象&#xff09;中的nickName 假设该行 {prop: "wxUserInfo", label: …