问题的产生
最近在弄着一个新的系统,有个接口耗时比较久,要针对它进行优化,优化嘛,多线程先上,多线程不行再上缓存。于是乎,很快啊,我就啪啪啪的写完了多线程的代码了,但是运行后发现,也没快多少啊。
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);
这样的代码运行效果跟上面的一样的同时也能阻塞主线程等待每个任务的完成结果,就是我想要的效果。
接口耗时问题解决了,但是新的问题又来了,为什么CompletableFuture
和Stream
同时使用会有这个问题呢,还是其他原因导致的?
为了研究问题究竟出现在哪,我用CompletableFuture
和Stream
写了针对这个案例写了多种搭配的用法。
不同写法的基准测试
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());
按我的想法,执行步骤应该是这样的:
- 先把list中的元素都进行状态的过滤
- 全部过滤后,执行
toString()
方法后,拼接123123
- 将前两步操作的得到的结果汇总成一个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实际运行中,只有执行到collect
、forEach
等终端方法时,才会给每个元素执行流上的操作。
说人话就是,在Stream中,每个元素都是按顺序一个一个 的执行全部逻辑 ,并非统一执行完一个 逻辑后,再统一执行下个逻辑。
例如上面的Stream代码,实际执行步骤则是:
- 第一个元素按照顺序分别执行Stream中的filter、map、collect方法。
- 第二个元素重复执行第一个步骤,直到全部元素都执行完毕。
那么,对于我第一版优化的代码来说,在Stream中的实际操作 则是先利用CompletableFuture
异步执行了方法后,马上就调用join
方法等待执行结果。实际上,这样子的写法跟单线程是没有区别的。若是非得使用Stream
和CompletableFuture
的话,就得写成这样:
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呢?因为业务中要用到线程间的变量,需要指定对应的线程池(在线程池中做了变量传递),而且默认的线程池能不用就不用,所以还是
最后附上一下优化前后的耗时对比,多线程果然是利器!
优化前

优化后
