Stream和CompletableFuture结合起来引发的问题

问题的产生

最近在弄着一个新的系统,有个接口耗时比较久,要针对它进行优化,优化嘛,多线程先上,多线程不行再上缓存。于是乎,很快啊,我就啪啪啪的写完了多线程的代码了,但是运行后发现,也没快多少啊。

java 复制代码
List<Object> list;
Executor executor;
list.stream()
    .filter(v -> v.isEnable())
    .map(v -> CompletableFuture.runAsync(v::dosomething, executor))
    .forEach(CompletableFuture::join);

写完之后,丢到流水线部署,成功之后,再次调用接口发现,时间没啥变化,甚至还久了一些,不应该啊。

问题定位

控制变量法

加了多线程后居然还会有这样的结果,我自己也很惊讶,那个时候我还觉得是CompletableFuture的问题,然后将代码改成了这样:

java 复制代码
List<Object> list;
Executor executor;
list.stream()
    .filter(v -> v.isEnable())
    .forEach(v -> executor.execute(v::dosomething);

push上去然后部署代码,一试,时间快了很多,就是我想要的优化效果。但是吧,这样的写法并不能保证异步线程的任务都执行完毕,所以还是改回用CompletableFuture抛弃Stream

java 复制代码
List<Object> list;
Executor executor;
List<CompletableFuture<Void>> futureList = new ArrayList<>();
list.removeIf(v -> !v.isEnable());
for (Object v : list) {
    futureList.add(CompletableFuture.runAsync(v::dosomething, executor));
}
futureList.forEach(CompletableFuture::join);

这样的代码运行效果跟上面的一样的同时也能阻塞主线程等待每个任务的完成结果,就是我想要的效果。

接口耗时问题解决了,但是新的问题又来了,为什么CompletableFutureStream同时使用会有这个问题呢,还是其他原因导致的?

为了研究问题究竟出现在哪,我用CompletableFutureStream写了针对这个案例写了多种搭配的用法。

不同写法的基准测试

Benchmark仓库:github.com/AzirZsk/Str...

总共写了5种写法:

序号 方法名称 代码逻辑
1 normal For循环
2 executor For循环+线程池执行
3 normalCompletableFuture For循环+CompletableFuture
4 streamCompletableFuture Stream+CompletableFuture
5 streamForEach Stream的ForEach

测试结果:

从测试结果能看出,For循环 的写法和Stream+CompletableFuture的写法耗时是差不多的,可以近似理解成,这两个写法是完全相等的。

为什么会这样

为什么这两个写法的运行耗时差不多呢?我先来说说我对于Stream内部的处理逻辑的理解。

在我认知中,Stream的操作应该是和普通的ForEach是差不多的,一行代码执行完后,再执行下一行。

比如用下面的代码来演示一下:

java 复制代码
Set<String> set = list.stream()
    .filter(v -> v.isEnable())
    .map(v -> {
        String str = v.toString();
        return str + "123123";
    })
    .collect(Collectors.toSet());

按我的想法,执行步骤应该是这样的:

  1. 先把list中的元素都进行状态的过滤
  2. 全部过滤后,执行toString()方法后,拼接123123
  3. 将前两步操作的得到的结果汇总成一个Set对象。

说白了就是每一个方法就是在这个阶段处理完成所有元素后再将处理得到的结果传递给下一个操作,可以理解为跟下面的代码是等价的:

java 复制代码
// 先过滤元素 => stream的filter方法
list.removeIf(v -> v.isEnable());
// 处理逻辑 => stream的map、collect方法
Set<Strign> set = new HashSet<>();
for (Object v : list) {
    set.add(v.toString() + "123123");
}

那为什么实际运行情况跟我设想中的不太一样呢?经过我仔细阅读源码后 DeepSeek 后,发现事实并非如此,在Stream实际运行中,只有执行到collectforEach终端方法时,才会给每个元素执行流上的操作。

说人话就是,在Stream中,每个元素都是按顺序一个一个 的执行全部逻辑 ,并非统一执行完一个 逻辑后,再统一执行下个逻辑。

例如上面的Stream代码,实际执行步骤则是:

  1. 第一个元素按照顺序分别执行Stream中的filter、map、collect方法。
  2. 第二个元素重复执行第一个步骤,直到全部元素都执行完毕。

那么,对于我第一版优化的代码来说,在Stream中的实际操作 则是先利用CompletableFuture异步执行了方法后,马上就调用join方法等待执行结果。实际上,这样子的写法跟单线程是没有区别的。若是非得使用StreamCompletableFuture的话,就得写成这样:

java 复制代码
List<Object> list;
Executor executor;
List<CompletableFuture<Void>> futureList = list.stream()
    .map(v -> CompletableFuture.runAsync(v::dosomething, executor))
    .collect(Collectors.toList());
futureList.stream()
    .forEach(CompletableFuture::join);

得分别调用两次Stream操作,才能同时使用CompletableFuture和Stream。

后记

回到这个问题,要是没有这一次问题的出现,我这辈子对于Stream流都是停留在之前的印象当中了。

说到这里,可能有人会问了,为什么不用parallelStream呢?因为业务中要用到线程间的变量,需要指定对应的线程池(在线程池中做了变量传递),而且默认的线程池能不用就不用,所以还是


最后附上一下优化前后的耗时对比,多线程果然是利器!

优化前

优化后

相关推荐
五行星辰2 分钟前
SAX解析XML:Java程序员的“刑侦破案式“数据处理
xml·java·开发语言
向哆哆6 分钟前
Java 开发工具:从 Eclipse 到 IntelliJ IDEA 的进化之路
java·eclipse·intellij-idea
你是狒狒吗30 分钟前
HttpServletRequest是什么
java
你们补药再卷啦42 分钟前
springboot 项目 jmeter简单测试流程
java·spring boot·后端
网安密谈1 小时前
SM算法核心技术解析与工程实践指南
后端
菜鸡且互啄691 小时前
sql 向Java的映射
java·开发语言
bobz9651 小时前
Keepalived 检查和通知脚本
后端
AKAMAI1 小时前
教程:在Linode平台上用TrueNAS搭建大规模存储系统
后端·云原生·云计算
盘盘宝藏1 小时前
idea搭建Python环境
后端·intellij idea
喵手1 小时前
Spring Boot 项目基于责任链模式实现复杂接口的解耦和动态编排!
spring boot·后端·责任链模式