parallelStream作用

采用多线程可以加快处理集合操作,底层原理是使用线程池ForkJoinPool(深入原理期待你的分享)

并行流一定会比Stream快吗?

在处理数据量并不大的情况下,“parallelStream()”的代码有时比使用“stream()”的代码慢。
因为:parallelStream()总是需要执行比按顺序执行更多的,在多个线程之间分割工作并合并或组合结果会带来很大的开销。像将短字符串转换为小写字符串这样的用例非常小,与并行拆分开销相比,它们可以忽略不计。

JAVA 并行流判断 java并行流底层原理_JAVA 并行流判断

使用多个线程处理数据可能会有一些初始设置成本,例如初始化线程池。这些开销可能会抑制使用这些线程所获得的收益,特别是在运行时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占用超高,如下图

JAVA 并行流判断 java并行流底层原理_JAVA 并行流判断_02

原因: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,锁,循环锁,外部操作(访问网络)卡住等。