记一次生产中使用CompletableFuture遇到的坑

news2025/7/19 14:29:20

为什么使用CompletableFuture

业务功能描述:有一个功能是需要调用基础平台接口组装我们需要的数据,在这个功能里面我们要调用多次基础平台的接口,我们的入参是一个id,但是这个id是一个集合。我们都是使用RPC调用,一般常规的想法去遍历循环这个idList,但是呢这个id集合里面的数据可能会有500个左右。说多不多,说少也不少,主要是在for循环里面多次去RPC调用是一件特别费时的事情。

我用代码大致描述一下这个需求:

 public List<BasicInfo> buildBasicInfo(List<Long> ids) {
         List<BasicInfo> basicInfoList = new ArrayList<>();
         for (Long id : ids) {
             getBasicData(basicInfoList, id);
         }
     }
 ​
     private List<BasicInfo> getBasicData(List<BasicInfo> basicInfoList, Long id) {
         BasicInfo basicInfo = rpcGetBasicInfo(id);
         return basicInfoList.add(basicInfo);
     }
 ​
     public BasicInfo rpcGetBasicInfo(Long id) {
         // 第一次RPC 调用
          rpcInvoking_1()...........
 ​
         // 拿到第一次的结果进行第二次RPC 调用
          rpcInvoking_2()...........
 ​
         // 拿到第二次的结果进行第三次RPC 调用、
          rpcInvoking_3()...........
 ​
         // 拿到第三次的结果进行第四次RPC 调用、
          rpcInvoking_4()...........
 ​
         // 组装结果返回
 ​
         return BasicInfo;
     }
复制代码

是的,这个数据的获取就是这么的扯淡。。。如果使用循环的方式,当ids数据量在500个左右的时候,这个接口返回的时间再8s左右,这是万万不能接受的,那如果ids数据更多呢?所以不能用for循环去遍历ids呀,这样确实是太费时了。

既然远程调用避免不了,那就想办法让这个接口快一点,这时候就想到了多线程去处理,然后就想到使用CompletableFuture异步调用:

CompletableFuture多线程异步调用

       List<BasicInfo> basicInfoList = new ArrayList<>();
       CompletableFuture<List<BasicInfo>> future = CompletableFuture.supplyAsync(() -> {
             ids.forEach(id -> {
                 getBasicData(basicInfoList, id);
             });
             return basicInfoList;
        });
        try {
            List<BasicInfo> basicInfos = future.get();
        } catch (Exception e) {
             e.printStackTrace();
        } 
复制代码

这里补充一点:CompletableFuture是否使用默认线程池的依据,和机器的CPU核心数有关。当CPU核心数减1大于1时,才会使用默认的线程池(ForkJoinPool),否则将会为每个CompletableFuture的任务创建一个新线程去执行。即,CompletableFuture的默认线程池,只有在双核以上的机器内才会使用。在双核及以下的机器中,会为每个任务创建一个新线程,等于没有使用线程池,且有资源耗尽的风险

默认线程池,池内的核心线程数,也为机器核心数减1,这里我们的机器是8核的,也就是会创建7个线程去执行。

上面这种方式虽然实现了多线程异步执行,但是如果ids集合很多话,依然会很慢,因为future.get();也是堵塞的,必须等待所有的线程执行完成才能返回结果。

改进CompletableFuture多线程异步调用

想让速度更快一点,就想到了把ids进行分隔:

   int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1;
   List<List<Long>> partitionAssetsIdList = Lists.partition(ids, pageSize);
复制代码

因为我们CPU核数为8核,所有当ids的大小小于8时,就开启8个线程,每个线程分一个。这里的>>3(右移运算)相当于ids的大小除以2的3次方也就是除以8;右移运算符相比除效率会高。毕竟现在是在优化提升速度。

如果这里的ids的大小是500个,就是开启9个线程,其中8个线程是处理62个数据,另一个线程处理4个数据,因为有余数会另开一个线程处理。具体代码如下:

         int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1;
         List<List<Long>> partitionIdList = Lists.partition(ids, pageSize);
         List<CompletableFuture<?>> futures = new ArrayList<>();
         //如果ids为500,这里会分隔成9份,也就是partitionIdList.size()=9;遍历9次,也相当于创建了9个CompletableFuture对象,前8个CompletableFuture对象处理62个数据。第9个处理4个数据。
         partitionIdList.forEach(partitionIds -> {
             List<BasicInfo> basicInfoList = new ArrayList<>();
             CompletableFuture<List<BasicInfo>> future = CompletableFuture.supplyAsync(() -> {
                 partitionIds.forEach(id -> {
                     getBasicData(basicInfoList, id);
                 });
                 return basicInfoList;
             });
             futures.add(future);
         });
         // 把所有线程执行的结果进行汇总
         List<BasicInfo> basicInfoResult = new ArrayList<>();
         for (CompletableFuture<?> future : futures) {
             try {
                 basicInfoResult.addAll((List<BasicInfo>)future.get());
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }
复制代码

如果ids的大小等于500,就会被分隔成9份,创建9个CompletableFuture对象,前8个CompletableFuture对象处理62个数据(id),第9个处理4个数据(id)。这62个数据又会被分成7个线程去执行(CPU核数减1个线程)。经过分隔之后充分利用了CPU。速度也从8s减到1-2s。得到了总监和同事的夸赞,同时也被写到正向事件中;哈哈哈哈。

在生产环境中遇到的坑。

上面说了那么多还没有说到坑在哪里,下面我们就说说坑在哪里?

本地和测试都没有啥问题,那就找个时间上生产呗,升级到生产环境,发现这个接口堵塞了,超时了。。。

刚被记录到正向事件,可不想在被记录个负向时间。感觉去看日志。

发现日志就执行了将ids进行分隔,后面循环去创建CompletableFuture对象之后的代码都没有在执行了。然后我第一感觉测试是future.get()获取结果的时候堵塞了,所以一直没有结果返回。

排查问题过程

我们要解决这个问题就要看看问题出现在哪里?

当执行到这个接口时候我们第一时间看了看CPU的使用率:

这是访问接口之前:

发现执行这个接口时PID为10348的这个进程的CPU突然的高了起来。

紧接着使用jps -l :打印出我们服务进程的PID

PID为10348正式我们现在执行这个服务。

接着我就详细的看一下这个PID为10348的进程下哪里线程占用的高:

发现这几个占用的相对高一点:

紧接着使用jstack命令生成java虚拟机当前时刻的线程快照,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源

jstack -l 10348 >/tmp/10348.log,使用此命令将PID为10348的进程下所有线程快照输出到log文件中。

同时我们将线程比较的PID转换成16进制:printf "%x\n" 10411

我们将转换成16进制的数值28ab,28a9在10348.log中搜索一下:

看到线程的快照发现这不是本次修改的接口呀。看到日志4处但是也是用了CompletableFuture。找到对应4处的代码发现这是监听mq消息,然后异步去执行,代码类型这样:

经过查看日志发现这个mq消息处理很频繁,每秒都会有很多的数据上来。

我们知道CompletableFuture默认是使用ForkJoinPool作为线程池。难道mq使用ForkJoinPool和我当前接口使用的都是同一个线程池中的线程?难道是共用的吗?

MQ监听使用的线程池:

我们当前接口使用的线程池:

它们使用的都是ForkJoinPool.commonPool()公共线程池中的线程!

看到这个结论就很好理解了,我们目前修改的接口使用的线程池中的线程全部都被MQ消息处理占用,我们修改优化的接口得不到资源,所以一直处于等待。

同时我们在线程快照10348.log日志中也看到我们优化的接口对应的线程处于WAITING状态!

这里- parking to wait for <0x00000000fe2081d8>肯定也是MQ消费线程中的某一个。由于MQ消费消息比较多,每秒都会监听到大量的数据,线程的快照日志收集不全。所以在10348.log中没有找到,这不影响我们修改bug。问题的原因已经找到了。

解决问题

上面我们知道两边使用的都是公共静态线程池,我们只要让他们各用各的就行了:自定义一个线程池:ForkJoinPool pool = new ForkJoinPool();

         int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1;
         List<List<Long>> partitionIdList = Lists.partition(ids, pageSize);
         List<CompletableFuture<?>> futures = new ArrayList<>();
         partitionIdList.forEach(partitionIds -> {
             List<BasicInfo> basicInfoList = new ArrayList<>();
             //重新创建一个ForkJoinPool对象就可以了
             ForkJoinPool pool = new ForkJoinPool();
             CompletableFuture<List<BasicInfo>> future = CompletableFuture.supplyAsync(() -> {
                 partitionIds.forEach(id -> {
                     getMonitoringCoverage(basicInfoList, id);
                 });
                 return basicInfoList;
            //在这里使用
             },pool);
             futures.add(future);
         });
         // 把所有线程执行的结果进行汇总
         List<BasicInfo> basicInfoResult = new ArrayList<>();
         for (CompletableFuture<?> future : futures) {
             try {
                 basicInfoResult.addAll((List<BasicInfo>)future.get());
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }
复制代码

这样他们就各自用各自的线程池中的线程了。不会存在资源的等待现场了。

总结:

之所以测试环境和开发环境没有出现这样的问题是因为这两个环境mq没有监听到消息。大量的消息都在生产环境中才会出现。由于测试环境的数据量达不到生产环境的数据量,所以有些问题在测试环境体验不出来。

码字不易,多多支持。还是那句话:不积跬步,无以至千里.不积小流,无以成江海!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/8100.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【22年11月12日更新】搭建宝塔面板、青龙面板“京东代挂”

本文章仅供学习 一、青龙面板是什么&#xff1f; 青龙面板可以运行某东脚本&#xff0c;你在某宝、某度等各个渠道搜索“京东代挂”&#xff0c;都是用青龙面板。 二、搭建宝塔面板 1.更新 yum 包 首先下载finalshell通过账号密码连接服务器&#xff0c;然后输入 yum up…

零基础程序员想要学好.Net,跟着这7个步骤学习就可以了

作为一个初学者程序员&#xff0c;很喜欢问的一个问题就是&#xff1a;零基础如何自学编程&#xff1f;在后台也有很多读者私信我&#xff0c;问我这个问题&#xff0c;其实这个问题比较大&#xff0c;不是一两句就可以说清楚的。 所以&#xff0c;今天结合我个人的经历&#x…

注意力机制详解(Attention详解)

注意力机制与人眼类似&#xff0c;例如我们在火车站看车次信息&#xff0c;我们只关注大屏的车次信息&#xff0c;而忽略大屏外其他内容&#xff0c;从而导致钱包被偷。。。 注意力机制只关注重点信息&#xff0c;忽略不重要的信息&#xff0c;关注最核心的内容。 主要就是这…

推荐系统实战2——EasyRec 推荐框架环境配置

推荐系统实战2——EasyRec 推荐框架环境配置学习前言先验条件EasyRec仓库地址EasyRec环境配置一、EasyRec的下载二、EasyRec的初始化三、EasyRec的安装四、一些额外的情况学习前言 EasyRec是阿里巴巴开源的推荐系统框架。生命苦短&#xff0c;从建好的推荐系统框架开始学&…

【C++】STL简介 -- string 的使用及其模拟实现

文章目录一、STL 简介1、什么是 STL2、STL 的版本3、STL 的六大组件4、STL 的重要性5、如何学习 STL二、string 类的使用1、什么是 string2、string 类模板3、构造函数4、Iterators5、Capacity6、Element Access7、Modify8、String Operations9、Non-member function overloads…

Arduino程序设计(二) 按键控制LED灯

按键控制LED灯程序设计前言一、按键控制LED灯——内部上拉&#xff08;基础&#xff09;二、按键控制LED灯——外部上拉&#xff08;基础&#xff09;三、按键控制LED灯&#xff08;进阶&#xff09;总结参考文献前言 本文主要介绍三种按键控制LED灯的实现方式&#xff0c;分别…

PatchCore原理与代码解读

paper&#xff1a;Towards Total Recall in Industrial Anomaly Detection code&#xff1a;GitHub - amazon-science/patchcore-inspection 存在的问题 目前无监督缺陷检测常用的一种方法是直接利用在ImageNet上预训练的模型中的表示&#xff0c;而不专门进行目标分布的迁…

从零开始将图片信息和空间信息绑定,并在前端展示到地图

作者&#xff1a;xiaoyan 关键词&#xff1a;前端查询时展示和空间数据绑定的图片资源 本文适合零基础入门 背景&#xff1a;iServer支持空间查询&#xff0c;可以将空间数据属性表中的属性查询出来&#xff0c;如通过SQL语句查询出某地大楼实际层高&#xff0c;或者查询出某…

RHCE实验--配置nfs服务

1、开放/nfs/shared目录&#xff0c;供所有用户查询资料&#xff1b; 2、开放/nfs/upload目录&#xff0c;供所有用户上传下载资料&#xff1b; 服务器与客户端都写好yum源以及挂载光盘&#xff0c;然后安装服务包 [rootserver ~]# yum install rpcbind -y [rootserver ~]# y…

Vue3基础

Vue 官网 https://cn.vuejs.org/ https://v3.cn.vuejs.org/ https://staging-cn.vuejs.org/api/ 1、环境 1.1、nodejs node node -vnpm #当前版本 npm -v #升级npm版本 npm install -g npm1.2、vue #安装vue npm install -g vue-cli #安装最新版本 npm install -g vu…

LQ0197 锦标赛【程序填空】

题目来源&#xff1a;蓝桥杯2014初赛 C A组E题 题目描述 本题为代码补全填空题&#xff0c;请将题目中给出的源代码补全&#xff0c;并复制到右侧代码框中&#xff0c;选择对应的编译语言&#xff08;C/Java&#xff09;后进行提交。若题目中给出的源代码语言不唯一&#xff0…

Python简单实现人脸识别检测, 对照片进行评分

大家好&#xff0c;今天和大家说说如何用Python简单实现人脸识别检测, 对照片进行排名&#xff0c;看看自己有多漂亮。 [开发环境]: Python 3.8 Pycharm 2021.2 [模块使用]: requests >>> pip install requeststqdm >>> pip install tqdm 简单实现进度条效果…

Arduino从零开始(1)——按钮控制LED

0.前言 本文主要介绍Arduino对于开关和条件判断函数的使用。 目录 0.前言 1.介绍 2.按钮控制LED 2.1下拉模式&#xff1a; 2.2上拉模式 3.扩展实验&#xff1a; 1.介绍 前篇介绍了点亮LED&#xff0c;这次案例我们尝试通过一个简单的传感器——按钮&#xff0c;来实现…

Ubuntu20.04离线安装Vmware tools

参考连接&#xff1a;在 Linux 虚拟机中手动安装 VMware Tools 从 Workstation Pro 菜单栏中选择虚拟机 > 安装 VMware Tools。 如果安装了早期版本的Vmware tools&#xff0c;则菜单项是更新Vmware tools如果这个安装Vmware tools 是灰色的&#xff0c;进行如下的处理方式…

HTML基本骨架与编辑器选择

HTML基本骨架与编辑器选择 文章目录HTML基本骨架与编辑器选择1.HTML基本了解1.1 什么是HTML1.2 HTML标签1.3 HTML元素1.4 Web浏览器1.5 HTML网页结构1.6 HTML版本了解2.HTML基本骨架介绍3.HTML编辑器的下载与使用1.HTML基本了解 1.1 什么是HTML HTML 是用来描述网页的一种语言…

双链表的基本操作

目录 一、双链表的设计 二、双链表的实现和基本操作 1.实现双链表节点以及设置first、last指针 2.获取当前链表中元素的数量 3.获取指定位置的节点 4.在尾部添加结点元素 5.在指定位置添加元素 6.删除指定位置的结点 一、双链表的设计 针对于查询操作&#xff0c;我们可…

计算机操作系统:实验3 【虚拟存储器管理】

计算机操作系统&#xff1a;实验3 【虚拟存储器管理】 文章目录计算机操作系统&#xff1a;实验3 【虚拟存储器管理】一、前言二、实验目的三、实验环境四、实验内容五、实验说明1、设计中虚页和实页的表示2、关于缺页次数的统计3、LRU算法中“最近最久未用”页面的确定4、算法…

删除类及其对象的属性:delattr()函数

【小白从小学Python、C、Java】 【Python-计算机等级考试二级】 【Python-数据分析】 删除类及其对象的属性 delattr()函数 [太阳]选择题 请问对以下Python代码说法错误的是&#xff1f; class MyClass1: x 1 y 2 myObject1 MyClass1() print(【访问】myObject1的属…

Revit中“结构框架显示与剪切“的应用和一键剪切功能

一、Revit关于"结构框架显示与剪切"的应用 结构框架&#xff1a;顾名思义其实它表示的就是结构梁而已&#xff0c;但是我们画图的时候往往会显示"实线"和"虚线"&#xff0c;以至于在出结构图纸的时候&#xff0c;达不到出图要求 NO.2、应用 Part…

ISCTF

upload upload,一道phar文件上传题目 <?php class upload{public $filename;public $ext;public $size;public $Valid_ext;public function __construct(){$this->filename $_FILES["file"]["name"];$this->ext end(explode(".", …