CompletableFuture你真的懂了么,我劝你在项目中慎用

news/2025/3/25 8:31:57/文章来源:https://www.cnblogs.com/wenbochang/p/18788003

1. 前言

在实际做项目中,我们经常使用多线程、异步的来帮我们做一些事情。

比如用户抽取奖品,异步的给他发一个push。

又比如一段前后不相关的业务逻辑,原本是顺序执行,耗时=(A + B + C),现在使用多线程加快执行速度,耗时=Max(A, B, C)。

这时候很多时候为了方便,我们就直接使用CompletableFuture来处理,但它真的好多坑,让我们一一细说。

2. CompletableFuture原理

2.1 CompletableFuture API

在CompletableFuture中提交任务有以下几种方式

public static CompletableFuture<Void> runAsync(Runnable runnable) 
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) 
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) 
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

这四个方法都是用来提交任务的,不同的是supplyAsync提交的任务有返回值,runAsync提交的任务没有返回值。两个接口都有一个重载的方法,第二个入参为指定的线程池,如果不指定,则默认使用ForkJoinPool.commonPool()线程池。

2.2 ForkJoinPool

Fork/Join 框架是一种并行计算框架,设计目的是提高具有递归性质任务的执行速度。典型的任务是将问题逐步分解成较小的任务,直到每一个子任务足够简单可以直接解决,然后再将结果聚合起来。

Fork/Join 框架基于"工作窃取"算法 (Work Stealing Algorithm),该算法的核心思想是每个工作线程有自己的任务队列(双端队列, Deque)。当一个线程完成了自己队列中的任务时,便会窃取其他线程队列中的任务执行,这样就不会因为某个线程在等待而浪费 CPU 资源。

Fork/Join 框架非常适合以下这些工作负载:

  • 递归任务:如斐波那契数列、归并排序等分治算法。
  • 大规模数据处理:快速对集合、数组等进行并行操作。
  • 图像处理:图像处理等数据量大的任务可以被分成多个小任务并行处理。

也就是说ForkJoinPool比较适用于CPU密集型,而不太适合于IO密集型。但是我们业务中大多数都是IO密集型,比如等待数据库的返回,等待下游RPC的返回,等待子方法的返回等等

2.3 ForkJoinPool在CompletableFuture中的应用

先说结论:

  1. 如果你在使用CompletableFuture没有指定线程池,就会使用默认的ForkJoinPool

  2. CompletableFuture是否使用默认线程池的依据,和机器的CPU核心数有关。当CPU核心数-1大于1时,才会使用默认的线程池,否则将会为每个CompletableFuture的任务创建一个新线程去执行

  3. 如果你的CPU核心数为4核,那么也就是最多也只有3个核心线程(3个线程,你确定够用?)

3. CompletableFuture坑

3.1 ForkJoinPool线程不够用,处于等待状态

小明为了加快代码的运行,将原来的A+B+C的运行逻辑,改成了(A,B,C)的运行逻辑,使用了3个CompletableFuture来执行,耗时从原本的900ms,缩短到了300ms,简单代码如下:

public void test1() {a();b();c();
}public void test2() {CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> a());CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> b());CompletableFuture<Integer> f3 = CompletableFuture.supplyAsync(() -> c());
}

上线后,小明心情良好,坐等升职加薪,没想到第二天却遇到了线上告警,接口频繁的超时,比之前还要慢,有些达到了10s+,小明实在想不明白。

后来排查发现,在项目中有大量使用到CompletableFuture.supplyAsync的地方,而每台机器8核,也就是7个线程,根本不够用,因此大量都是在等待线程中度过,因此耗时越来越严重,最终形成了雪崩,接口直接无限超时。

回答:使用CompletableFuture必须做到线程池隔离,不能使用默认的ForkJoinPool线程池

3.2 CompletableFuture反而更慢了?

小明经过这次事件学聪明了,使用CompletableFuture都自己写一个线程池。过了几天线上又告警出来了,大量接口超时,小明又蒙逼了,小明的代码如下:

ExecutorService es = Executors.newFixedThreadPool(5);
public void test1() {CompletableFuture.runAsync(a(1), es);CompletableFuture.runAsync(b(1), es);CompletableFuture.runAsync(c(1), es);
}

后来排查发现,springmvc tomcat默认线程池是200,而你的线程池只有5个,也就是说,当接口请求了攀升。

比如现在有200个请求过来,执行到test1的时候,如果不使用线程池,反而没任何问题。但是使用到了线程池,5个线程池根本不够用,等待线程的释放,那么会越来越慢,最终拖垮整个服务。

3.3 CompletableFuture死锁?

小明说我再也不使用CompletableFuture了,小明说我直接调大线程池到200,那肯定没问题了,读者们思考下是否可行。答案是绝对不可行的,核心线程设置那么大,对cpu消耗非常严重,一定要设置合理的范围内。

再来看一个死锁问题,终于不是小明的锅了,这次轮到了小红,以下是死锁的代码:

ExecutorService es = Executors.newFixedThreadPool(5);
public void test() {for (int i = 0; i < 5; i++) {CompletableFuture.runAsync(() -> a(), es); }
}public void a() {CompletableFuture<Integer> f = CompletableFuture.supplyAsync(() -> 1, es);try {f.get();} catch (Exception e) {}
}

这里不卖关子了。由于是5个线程,在test方法中,将5个线程全部使用,然后test调用子方法a的时候。由于共用同一个线程池es,a方法永远获取不到线程池,a方法永远不可能执行成功,那么test方法也永远执行不了成功,那么就会处于永远阻塞死锁的这么一个线程。

因此解决办法就是,不同的业务尽量不要使用同一个线程池,为自己业务定制自己的线程池,而不是为了方便,共用一个commonPool。

4. 最后

通过以上对CompletableFuture的分析,以及一些实际踩坑的案例,相信你对CompletableFuture用法更加的了解了。

最后还是想说明一点,在业务代码中,能不使用多线程就不使用多线程,因为它带来的副作用远远比带来的好处要多的多得多,除非你非常清楚其中的原理。

CompletableFuture你真的懂了么?欢迎评论区留言讨论。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第四周第三章3.1—3.5

3.1 initial_weight = 50 print("年份\t地球体重(kg)\t月球体重(kg)") for year in range(1, 11): earth_weight = initial_weight + (year - 1) * 0.5 moon_weight = earth_weight * 0.165 print(f"{year}\t\t{earth_weight:.2f}\t\t{moon_weight:.2f}")3…

PolarCTF网络安全2025春季个人挑战赛 WRITE UP

1-1 可老师签到 本题思路如下: 提示“发送的内容为双写字符串拼接”公众号发送flagflag即可1-2 find 本题思路如下: 把表格文件当压缩包解压,找到flag.xlsx\xl\worksheets\sheet1.xml 发现里面存了数据,于是考虑把数据格子上色以得到flag 先将xml文件处理以获得纯数据代码:…

Go红队开发—CLI框架(一)

CLI命令行工具编写基础学习(一)。CLI开发框架 命令行工具开发,主要是介绍开发用到的包,集成了一个框架,只要学会了基本每个人都能开发安全工具了。 该文章先学flags包,是比较经典的一个包,相比后面要学习的集成框架这个比较自由比较细化点,自定义可能高一些,后续会学到一…

WSL2安装Ubuntu

本文是介绍如何使用任意一台Windows主机借助WSL2创建Ubuntu虚拟系统并开放局域网内SSH连接的操作指南!先决条件拥有能够访问github.com的网络环境; 必须运行 Windows 10 版本 2004 及更高版本(内部版本 19041 及更高版本)或 Windows 11版本;安装WSL2 以管理员身份运行Powe…

人工‘够好就行’智能(AGEI)就快来了!

比尔弗兰克斯 前言:本文作者的观点是在真正的通用人工智能(AGI)到来之前,一种“够好就行”的人工智能(AGEI)就已经足以引发我们原本担心 AGI 才会带来的重大社会变革、正面效益,甚至潜在危机。 换句话说: AI 不需要达到像人类一样全面智能的程度; 只要在足够多的任务上…

List集合--java进阶day09

1.List集合以下面的例子来解释存储有序和存储重复如上图,我们是怎么添加这些字符串的,打印的时候就是按照这个顺序打印的--存取有序 并且“张三”出现了两次,也存入了两次--存储重复 因为List有索引,所以该接口有关于索引的独特的api..1.remove注意事项 List有两个remove方…

Pydantic字段级校验:解锁@validator的12种应用

title: Pydantic字段级校验:解锁@validator的12种应用 date: 2025/3/23 updated: 2025/3/23 author: cmdragon excerpt: Pydantic校验系统支持通过pre验证器实现原始数据预处理,在类型转换前完成字符清洗等操作。格式验证涵盖正则表达式匹配与枚举值约束,确保护照编号等字…

20244126 2024-2025-2 《python程序设计》实验一报告

课程:《Python程序设计》 班级:2441 姓名:马晓霞 学号:20244126 实验教师:王志强 实验日期:2025年3月23日 必修/选修:公选课 (一)实验内容 1.熟悉Python开发环境 2.练习Python运行,调试技能 3.编写技能,练习变量和类型、字符串、对象、缩进和注释等 4.编写一个猜数字…

1.6K star!这个开源文本提取神器,5分钟搞定PDF/图片/Office文档!

Kreuzberg 是一个基于 Python 的文本提取库,支持从 PDF、图像、Office 文档等 20+ 格式中提取文本内容。采用 MIT 开源协议,具备本地处理、异步架构、智能 OCR 等特性,特别适合需要隐私保护的文档处理场景。嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开…

leetcode每日一题:判断一个括号字符串是否有效

题目 一个括号字符串是只由 ( 和 ) 组成的 非空 字符串。如果一个字符串满足下面 任意 一个条件,那么它就是有效的:字符串为 (). 它可以表示为 AB(A 与 B 连接),其中A 和 B 都是有效括号字符串。 它可以表示为 (A) ,其中 A 是一个有效括号字符串。给你一个括号字符串 s 和…

实验2c语言分支与循环基础应用编程

任务一:1 #include <stdio.h>2 #include <stdlib.h>3 #include <time.h>4 5 #define N 56 7 int main() {8 int number;9 int i; 10 11 srand(time(0)); // 以当前系统时间作为随机种子 12 for(i = 0; i < N; ++i) { 13 nu…

矿用人员违规闯入监控报警系统

矿用人员违规闯入监控报警系统,设置警戒预警功能,巷道正在行车时,当有行人闯入时,及时抓拍、识别现场违章入侵人员同时闪光警示和音箱报警提示禁止进入,在绞车运行期间人员误入斜巷能够及时报警,自动停止绞车运行。该系统做到绞车运行和行人的安全隔离,有力保障了煤矿斜…