CompletableFuture你真的懂了么,我劝你在项目中慎用
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中的应用
先说结论:
如果你在使用
CompletableFuture没有指定线程池,就会使用默认的ForkJoinPoolCompletableFuture是否使用默认线程池的依据,和机器的CPU核心数有关。当CPU核心数-1大于1时,才会使用默认的线程池,否则将会为每个CompletableFuture的任务创建一个新线程去执行。
如果你的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你真的懂了么?欢迎评论区留言讨论。
CompletableFuture你真的懂了么,我劝你在项目中慎用的更多相关文章
- [C#] C# 知识回顾 - 你真的懂异常(Exception)吗?
你真的懂异常(Exception)吗? 目录 异常介绍 异常的特点 怎样使用异常 处理异常的 try-catch-finally 捕获异常的 Catch 块 释放资源的 Finally 块 一.异常介 ...
- 【转】was mutated while being enumerated 你是不是以为你真的懂For...in... ??
原文网址:http://www.jianshu.com/p/ad80d9443a92 支持原创,如需转载, 请注明出处你是不是以为你真的懂For...in... ??哈哈哈哈, 我也碰到了这个报错 . ...
- javascript的语法作用域你真的懂了吗
原文:javascript的语法作用域你真的懂了吗 有段时间没有更新了,思绪一下子有点转不过来.正应了一句古话“一天不读书,无人看得出:一周不读书,开始会爆粗:一月不读书,智商输给猪.”.再加上周五晚 ...
- 你真的懂ajax吗?
前言 总括: 本文讲解了ajax的历史,工作原理以及优缺点,对XMLHttpRequest对象进行了详细的讲解,并使用原生js实现了一个ajax对象以方便日常开始使用. damonare的ajax库: ...
- “三次握手,四次挥手”你真的懂吗?TCP
“三次握手,四次挥手”你真的懂吗? mp.weixin.qq.com 来源:码农桃花源 解读:“拼多多”被薅的问题出在哪儿?损失将如何买单? 之前有推过一篇不错的干货<TCP之三次握手四次挥手 ...
- 你真的懂 ajax 吗?
前言 总括: 本文讲解了ajax的历史,工作原理以及优缺点,对XMLHttpRequest对象进行了详细的讲解,并使用原生js实现了一个ajax对象以方便日常开始使用. damonare的ajax库: ...
- 【转】先说IEnumerable,我们每天用的foreach你真的懂它吗?
[转]先说IEnumerable,我们每天用的foreach你真的懂它吗? 我们先思考几个问题: 为什么在foreach中不能修改item的值? 要实现foreach需要满足什么条件? 为什么Linq ...
- 程序猿修仙之路--数据结构之你是否真的懂数组? c#socket TCP同步网络通信 用lambda表达式树替代反射 ASP.NET MVC如何做一个简单的非法登录拦截
程序猿修仙之路--数据结构之你是否真的懂数组? 数据结构 但凡IT江湖侠士,算法与数据结构为必修之课.早有前辈已经明确指出:程序=算法+数据结构 .要想在之后的江湖历练中通关,数据结构必不可少. ...
- C# 知识回顾 - 你真的懂异常(Exception)吗?
你真的懂异常(Exception)吗? 目录 异常介绍 异常的特点 怎样使用异常 处理异常的 try-catch-finally 捕获异常的 Catch 块 释放资源的 Finally 块 一.异常介 ...
- 你真的懂printf么?
自从你进入程序员的世界,就开始照着书本编写着各种helloworld,大笔一挥: printf("Hello World!\n"); 于是控制台神奇地出现了一行字符串,计算机一句温 ...
随机推荐
- 如何通过C#修改Windows操作系统时间
C#的System.DateTime类提供了对日期时间的封装,用它进行时间的转换和处理很方便,但是我没有在其中找到任何可以用来修改系统时间的成员.用过VC.VB等的朋友可能知道,我们可以调用Win32 ...
- 基于sass tailwindcss的传统页面开发脚手架
这是一个基于sass和tailwindcss的快速开发传统多页面的npm脚手架. package.json { "name": "sass-tailwindcss-sta ...
- sshd 启动失败
解决方法 yum remove openssh yum install openssh openssh-server openssh-clients systemctl start sshd syst ...
- python基础学习4
打开文件的方式 # 第一种 f = open('C:\project\pycharmprojects\\bigdata33\day05/cars.csv', mode='r', encoding='U ...
- JVM最全知识体系考点复盘总结
1:什么是JVM JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现 ...
- 基于同态加密的PSI开源库-1
下面介绍一个PSI的开源库,还原论文:CCS2017:Fast Private Set Intersection from Homomorphic Encryption和CCS2018:Labeled ...
- DataV Note:让数据自己讲故事
您是否常常因为面对那些充满各类指标的汇报报告而感到困扰?我们或许能帮到您! 「我们是一家国内的服装公司,财年结束了,公司的销售团队需要对公司的销售数据进行分析,以指导下个财年的作战方向」 「我是浙 ...
- SourceTree SSH第一次登录需要交互确认的问题
问题 在SourceTree SSH配置完ssh之后向上提交代码的时候发现: The server's host key is not cached in the registry. You have ...
- 0101-JDK和tomcat的安装配置
一.JDK8安装与配置 分别配置如下三个系统变量 JAVA_HOME设置变量值为java JDK的安装目录例如: C:\Program Files\Java\jdk1.8.0 PATH添加变量值 %J ...
- 从0搭建Vue3组件库(一): 开篇
前言 这是从0搭建Vue3组件库系列文章第一篇文章,这个系列我曾经写过多篇文章,但是写完之后回过头来再看里面有很多遗漏以及不足之处,所以决定重新梳理这个系列,并从头开始搭建一个完整的Vue3组件库工程 ...