parallelStream作用
采用多线程可以加快处理集合操作,底层原理是使用线程池ForkJoinPool(深入原理期待你的分享)
并行流一定会比Stream快吗?
在处理数据量并不大的情况下,“parallelStream()”的代码有时比使用“stream()”的代码慢。
因为:parallelStream()总是需要执行比按顺序执行更多的,在多个线程之间分割工作并合并或组合结果会带来很大的开销。像将短字符串转换为小写字符串这样的用例非常小,与并行拆分开销相比,它们可以忽略不计。
使用多个线程处理数据可能会有一些初始设置成本,例如初始化线程池。这些开销可能会抑制使用这些线程所获得的收益,特别是在运行时CPU已经非常低的情况下。另外,如果有其他线程在运行后台进程等,或者争用很高,那么并行处理的性能会进一步降低。
线程安全性要认真考虑
不合理的使用数据类型导致,CPU占用高
如下代码在生成环境运行一段时间后,系统显示服务的cpu占用非常高,达到了100%。
Set<TruckTeamAuth> list = new HashSet<>(); // 1、声明变量
List<STruckDO> sTruckDOList = isTruckService.lambdaQuery().select(STruckDO::getId, STruckDO::getTeamId).isNotNull(STruckDO::getTeamId).in(STruckDO::getTeamId, teamIdList).list();
sTruckDOList.parallelStream().forEach(t -> { // 2、并行处理
if (StrUtil.isNotBlank(t.getId()) && StrUtil.isNotBlank(t.getTeamId())) {
list.add(TruckTeamAuth.builder().teamId(t.getTeamId()).truckId(t.getId()).build()); // 3、操作集合
}
});
通过jstack的日志信息定位到是操作HashSet时,内部资源竞争导致CPU占用超高,如下图
原因
:HashSet是非线程安全的,内部实际是通过HashMap实现,在多线程中操作了HashSet,导致红黑转换的竞争
空指针异常
并行流对列表会偶尔性报空指针异常,如下图
List<OrderListVO> orderListVOS = new LinkedList<OrderListVO>();
baseOrderBillList.parallelStream().forEach(baseOrderBill -> {
OrderListVO orderListVO = new OrderListVO();
// 设置order中的属性
orderListVO.setOrderbillgrowthid(baseOrderBill.getOrderbillgrowthid());
orderListVO.setOrderbillid(baseOrderBill.getOrderbillid());
……
orderListVOS.add(orderListVO);
}
代码本身是在做多表拆分然后业务层组装,使用并行流能够提升这种纯粹的CPU密集型操作,parallelStream 此方法默认是以服务器CPU核数为线程池大小的。
因为是并行流,所以其实是多线程在并发操作这个orderListVOS 容器,但是这个容器是不能保证线程安全的。`
解决方法
1.推荐
使用stream自带的聚合方法,如下
orderListVOS.parallelStream()
.sorted(Comparator.comparing(OrderListVO::getCreatetime).reversed())
.collect(Collectors.toList());
2.采用java.util.concurrent提供的特性(注意:该包提供的相关类是会有到锁的)
ParallelStream 风险
虽然parallelStream的流式编程带来的极大的多线程开发便利性,但同时也带来了一个隐含的逻辑,且并未在接口注释中说明:
/**
* Returns a possibly parallel {@code Stream} with this collection as its
* source. It is allowable for this method to return a sequential stream.
*
* <p>This method should be overridden when the {@link #spliterator()}
* method cannot return a spliterator that is {@code IMMUTABLE},
* {@code CONCURRENT}, or <em>late-binding</em>. (See {@link #spliterator()}
* for details.)
*
* @implSpec
* The default implementation creates a parallel {@code Stream} from the
* collection's {@code Spliterator}.
*
* @return a possibly parallel {@code Stream} over the elements in this
* collection
* @since 1.8
*/
以上是该接口的全部注释,这里所谓的隐含逻辑是,并非每一个独立调用parallelStream的代码都会独立维护运行一个多线程的策略,而是JDK默认会调用同一个由运行环境维护的ForkJoinPool线程池,也就是说,无论在哪个地方写了list.parallelStream().forEach();这样一段代码,底层实际都会由一套ForkJoinPool的线程池进行运行,一般线程池运行会遇到的冲突、排队等问题,这里同样会遇到,且会被隐藏在代码逻辑中。
这里最危险的当然就是线程池的deadlock,一旦发生deadlock,所有调用parallelStream的地方都会被阻塞,无论你是否知道其他人是否这样书写了代码。
以这段代码为例
list.parallelStream().forEach(o -> {
o.doSomething();
...
});
只要在doSomething()中有任何导致当前执行被hold住的情况,则由于parallelStream完成时会执行join操作,任何一个没有完成迭代都会导致join操作被hold住,进而导致当前线程被卡住。
典型的操作有:线程被wait,锁,循环锁,外部操作(访问网络)卡住等。