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呢?因为业务中要用到线程间的变量,需要指定对应的线程池(在线程池中做了变量传递),而且默认的线程池能不用就不用,所以还是


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

优化前

优化后

相关推荐
aircrushin9 分钟前
端到端AI决策架构如何重塑实时协作体验?
前端·javascript·后端
苦瓜小生17 分钟前
【黑马点评学习笔记 | 实战篇 】| 6-Redis消息队列
redis·笔记·后端
大傻^38 分钟前
LangChain4j Spring Boot Starter:自动配置与声明式 Bean 管理
java·人工智能·spring boot·spring·langchain4j
沐硕39 分钟前
《基于改进协同过滤与多目标优化的健康饮食推荐系统设计与实现》
java·python·算法·fastapi·多目标优化·饮食推荐·改进协同过滤
yhole1 小时前
springboot 修复 Spring Framework 特定条件下目录遍历漏洞(CVE-2024-38819)
spring boot·后端·spring
BingoGo1 小时前
Laravel 13 正式发布 使用 Laravel AI 无缝平滑升级
后端·php
愣头不青1 小时前
560.和为k的子数组
java·数据结构
共享家95271 小时前
Java入门(String类)
java·开发语言
l软件定制开发工作室1 小时前
Spring开发系列教程(34)——打包Spring Boot应用
java·spring boot·后端·spring·springboot
0xDevNull1 小时前
Spring Boot 循环依赖解决方案完全指南
java·开发语言·spring