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你真的懂了么?欢迎评论区留言讨论。

CompletableFuture你真的懂了么,我劝你在项目中慎用的更多相关文章

  1. [C#] C# 知识回顾 - 你真的懂异常(Exception)吗?

    你真的懂异常(Exception)吗? 目录 异常介绍 异常的特点 怎样使用异常 处理异常的 try-catch-finally 捕获异常的 Catch 块 释放资源的 Finally 块 一.异常介 ...

  2. 【转】was mutated while being enumerated 你是不是以为你真的懂For...in... ??

    原文网址:http://www.jianshu.com/p/ad80d9443a92 支持原创,如需转载, 请注明出处你是不是以为你真的懂For...in... ??哈哈哈哈, 我也碰到了这个报错 . ...

  3. javascript的语法作用域你真的懂了吗

    原文:javascript的语法作用域你真的懂了吗 有段时间没有更新了,思绪一下子有点转不过来.正应了一句古话“一天不读书,无人看得出:一周不读书,开始会爆粗:一月不读书,智商输给猪.”.再加上周五晚 ...

  4. 你真的懂ajax吗?

    前言 总括: 本文讲解了ajax的历史,工作原理以及优缺点,对XMLHttpRequest对象进行了详细的讲解,并使用原生js实现了一个ajax对象以方便日常开始使用. damonare的ajax库: ...

  5. “三次握手,四次挥手”你真的懂吗?TCP

    “三次握手,四次挥手”你真的懂吗?  mp.weixin.qq.com 来源:码农桃花源 解读:“拼多多”被薅的问题出在哪儿?损失将如何买单? 之前有推过一篇不错的干货<TCP之三次握手四次挥手 ...

  6. 你真的懂 ajax 吗?

    前言 总括: 本文讲解了ajax的历史,工作原理以及优缺点,对XMLHttpRequest对象进行了详细的讲解,并使用原生js实现了一个ajax对象以方便日常开始使用. damonare的ajax库: ...

  7. 【转】先说IEnumerable,我们每天用的foreach你真的懂它吗?

    [转]先说IEnumerable,我们每天用的foreach你真的懂它吗? 我们先思考几个问题: 为什么在foreach中不能修改item的值? 要实现foreach需要满足什么条件? 为什么Linq ...

  8. 程序猿修仙之路--数据结构之你是否真的懂数组? c#socket TCP同步网络通信 用lambda表达式树替代反射 ASP.NET MVC如何做一个简单的非法登录拦截

    程序猿修仙之路--数据结构之你是否真的懂数组?   数据结构 但凡IT江湖侠士,算法与数据结构为必修之课.早有前辈已经明确指出:程序=算法+数据结构  .要想在之后的江湖历练中通关,数据结构必不可少. ...

  9. C# 知识回顾 - 你真的懂异常(Exception)吗?

    你真的懂异常(Exception)吗? 目录 异常介绍 异常的特点 怎样使用异常 处理异常的 try-catch-finally 捕获异常的 Catch 块 释放资源的 Finally 块 一.异常介 ...

  10. 你真的懂printf么?

    自从你进入程序员的世界,就开始照着书本编写着各种helloworld,大笔一挥: printf("Hello World!\n"); 于是控制台神奇地出现了一行字符串,计算机一句温 ...

随机推荐

  1. 在Deepin系统上配置微软Windows远程桌面服务

    . 前言 本文主要讲解如何在deepin系统上安装和配置Xrdp远程桌面. Xrdp是微软的远程桌面协议(Remote Desktop Protocol, RDP)的开源版本.在Linux系统上安装X ...

  2. SpringCloud-Ribbon

    1. Ribbon简介 Ribbon是一个基于HTTP和TCP的客户端负载均衡器,当使用Ribbon对服务进行访问的时候,他会扩展Eureka客户端的服务发现功能,实现从Eureka注册中心获取服务端 ...

  3. Applitools_问题汇总

    1.  Android使用Real Device 问题1: AttributeError: 'NoneType' object has no attribute 'to_capabilities' 解 ...

  4. Appium_测试步骤读取自外部文件:定制执行引擎

    testcase.yaml: - id: home_search - id: search_input_text input: alibaba - id: name - id: current_pri ...

  5. c# 通过注册表判断有没有安装某个软件

    private bool checkHasInstalledSoftWare(string displayName) { Microsoft.Win32.RegistryKey uninstallNo ...

  6. 使用_begin{thebibliography}__bibitem 如何参考文献

    本人是tex新手,如果各位大佬有更好的方法欢迎分享,不胜感激. 适用情况 本文适用于使用\begin{thebibliography}和\bibitem排序的情况,如果使用bibtex排序那么网上很多 ...

  7. JMeter组件的执行顺序和作用域

    组件介绍 测试计划:jmeter的起点和容器 线程组:代表一定的虚拟用户 取样器:发送请求的最小单元 逻辑控制器:控制组件的执行顺序 前置处理器:在请求之前的操作 后置处理器:在请求之后的操作 断言: ...

  8. 搭建基于Grafana+JMeter+InfluxDB的性能监控与分析平台(Linux)

    搭建基于Grafana+JMeter+InfluxDB的性能监控与分析平台(Linux版) 在软件开发和运维领域,性能监控与分析是确保应用稳定性和用户体验的关键环节.随着应用规模的不断扩大和复杂度的增 ...

  9. [记录点滴] OpenResty中Redis操作总结

    [记录点滴] OpenResty中Redis操作总结 0x00 摘要 本文总结了在OpenResty中的操作,与大家分享,涉及知识点为Openresty, Lua, Redis. 0x01 操作记录 ...

  10. CF1326G 题解

    题意: 蛛网树是一颗平面树,满足点是该树的凸包的顶点上等价于其是叶子. 给定一个平面树,求有多少种对点集的划分,使得每个划分出来的集合都是蛛网树. Solution 考虑树形 dp.设 \(f_u\) ...