【Java没基础】Java 8 并行流 ParallelStream

前言

在前两篇的 Java 8 函数式编程的 blog 中,我们聊了 Lambda 表达式,聊了一些常用的 Stream API 和一些收集器方法。这篇将是我们一起系统的了解学习 Java 8 函数式编程特性的最后一篇正式的 blog,以后再遇到什么问题或者学习到新的知识我会以填坑的形式以小篇 blog 来进行探讨和填坑~

并行流

我们通过前两篇了解了 Lambda 表达式的便捷、代码的优美,在今天,我们要考虑一下效率了。

在我们开始探讨这个话题之前,应该先看看这篇来自 ImportNew 的一篇翻译博文:《Java8 Lambda表达式和流操作如何让你的代码变慢5倍》

在文章的最后我们可以了解到,使用 parallelStream 来对大数据量 List 进行处理是拥有着最快的速度。当然了,我们需要考虑到其中未知的效率影响,但是使用并行流 parallelStream 对代码性能的提升是不可忽略的。

数据并行化

数据并行化是指将数据分成块,为每块数据分配单独的处理单元。它将问题分解为可在多块数据处理器上求解的形式,然后对每块数据执行运算,最后将各个数据处理器上得到的结果汇总,从而获得最终答案。

对于数据并行计算之后的运行速度,我们首先要了解一个概念:阿姆达尔定律

阿姆达尔定律预测了搭载多核处理器的机器提升程序速度的理论最大值。以完全串行的程序为例,如果将其一半数据改为并行化处理,不管增加了多少处理器,其理论上的最大速度只是原来的二倍。

即:问题的求解时间将完全取决于它可以被分解为几个部分。

使用并行流

将流进行并行化操作十分简单,如果有一个 Stream 流对象,那么调用他的parallel方法

stream.parallel()....

如果从一个集合类中创建一个具有并行处理能力的流,与创建一个流类似,只需要调用 parallelStream 方法,其他处理流的方法不变。

// 举个栗子 :在汽车列表中 找出每个车主拥有的汽车数量
public int parallelCarNumOfUser(List cars){
  return cars.parallelStream()
    .flatMap(User :: getUsers)
    .count();
}

实际上,并行流的底层还是沿用了 fork/join 框架,fork 分解问题、解决问题,最后由 join 合并为结果。
【Java没基础】Java 8 并行流 ParallelStream_第1张图片

《Java 8 函数式编程》第六章 fork/join 分解合并问题

并行流使用的是默认的 ForkJoinPool 来控制并行处理数量,这个默认的 ForkJoinPool 和 CPU 的核心数量相同,使用默认的 ForkJoinPool 适用于 全 CPU 占用的密集计算

流操作分为有 状态无状态 两种,
有状态包括 sorted、distinct、 limit 等;
无状态包括 map、 filter、faltMap等;
我们在操作并行流的时候,应该尽量的避开有状态,选用无状态

而应对于非密集计算的业务场景,可以采用自定义的 ForkJoinPool 来控制进行计算的 CPU 数量。

// 举个栗子,来段伪代码,自定义一个使用一定数量 CPU 的ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(int numbersOfCPU);
forkJoinPool.submit(() ->{
  ***.parallelStream()
    .***(* -> ***)
    .***()
}


并行流限制

并行流的加入和使用带来了效率,同时也带来了一些问题,比如我们想到:如果运算顺序的变化会导致结果的变化的时候,这样我们应该怎么办?

所以,使用并行流代码必须遵守一些规则和限制。

先举个简单的例子,我们使用 reduce 来进行求和操作。在串行流中,我们可以设定任何初始值

比如 : reduce(100, (a, b) -> a + b);

而当我们使用并行流来编写如上代码的时候就要小心了,因为有几路并行,代码的实现中就会加上几个 100,这样的话就会影响最终结果。
还有,如果运算顺序影响最终结果怎么办呢?

所以:1、在使用并行流的时候,初值必须为组合函数的恒等值, 一般情况下,加法的初始值都为 0 ,乘法的初始值都为1。(至于为什么?问一下小学老师就知道了。) 2、reduce 操作的另一个限制是组合要满足结合律。这样的话,交换运算顺序不会对最终结果产生影响。

除了上面比较具有针对性的两点,还有一点适用于并行流的通用情况,即 避免持有锁。流框架会在需要的时候,自己处理同步操作,因此我们不需要为自己的数据结构加锁,这会产生不必要的开销。

并行与串行流

如果我们希望有个流不进行并行操作,只进行并行显示进行串行操作的时候,我们可以使用 sequential 方法来将这个流显示定义为串行的。

使用 parallel 可以将流转换为并行流
使用 sequential 可以将流转换为串行流

在对流进行求值时,流不能同时处于两种模式,要么并行要么串行,如果同时调用了 parallel 和 sequential 方法,那么最后的那个起作用。

性能要素

影响并行流性能的主要因素有几点

1、数据大小
我们知道,将问题分解后进行处理再进行合并会带来额外的开销,那么在小数据量的时候,使用并行流的运行时间有可能超过单核串行运行时间数。所以只有在 数据量足够大 的时候,采用并行化处理才有意义。

2、源数据结构
每个管道(即并行之后的流) 的操作都基于初始数据源,通常是集合。而不同数据结构的集合在分解的时候有着不同的开销,这些开销同样影响了并行流处理数据的能力。

一般情况下
ArrayList > HashSer ≈ TreeSet > LinkedList

3、装箱
处理基本数据类型肯定要比处理装箱类型要快

4、核心数
根据阿姆达尔定律,能够使用的核心数越多,程序能获得的潜在性能提升的幅度就越大。而且,同时运行的其他进程(包括但不限于:操作系统、IDE、其他应用程序比如音乐或者其他 Java 进程),或者线程关联性都会影响到系统性能,影响到你的可用核心数量。

5、单元处理开销
单个元素的大小或者复杂程度同时也是影响并行流性能的一大主因,花在流中的每个元素的时间越长,并行操作所带来的性能提升就越明显

并行流操作数组

Java 8 引入 了一些针对数组的并行操作,可以脱离流框架单独使用。这些操作被封装在工具类 java.util.Arrays 中,并且提供了很多的重载方法供我们挑选使用。在 Arrays 类中,同样还包括 Java 以前提供的与数组相关的有用方法。

方法名 说明
parallelPrefix 任意给定一个函数,计算数据的和(数据累加)
parallelSetAll 使用 Lambda 表达式更新数组元素
parallelSort 并行化对数组排序


测试

Lambda 表达式的测试

对 Lambda 表达式的测试有些困难,因为 Lambda 表达式没有方法名,无法在测试代码中进行调用。然而代码测试是不可少的,我们来一起研究一下测试方法

方法一:复制 Lambda 表达式来进行测试
当你修改了实现代码,可是测试代码依然会通过,那么这个测试也就没有什么意义。

方式二:将 Lambda 表达式放到另一个方法中进行测试
这个看起来是可行的,但是我们仔细思考一下,这么做的话被测试的主体只是那个方法,而不是我们想要测试的 Lambda 表达式本身。

方式三:将Lambda表达式改写为一个普通方法,对这个方法进行测试
这个方法是应该被推荐的。一个 Lambda 表达式必然可以被改写成为一个有名字的普通方法,我们对这个方法进行测试就是对 Lambda 表达式本身进行测试,并且可以在测试用例中覆盖所有的边界情况。

我们也可以使用一个非常强大的测试框架:Mockito 框架来对 Lambda 表达式进行测试。
Mockito 的 Answer 接口允许用户提供其他行为,这里 Answer 之所以可以使用 Lambda 表达式,是因为 Answer 本事就是一个函数接口。

Stream 流的测试

当我们对一个流进行单步调试,我们会发现调试过程中出现了一些问题,因为流的一些操作是惰性求值的,我们无法通过打印值或者堆栈的方法来观察他是否执行了我们希望的操作。

忽然,我们想到,我们或许可以通过 ForEach 来逐步打印啊?

然而操作的时候,我们会发现有个问题出现了,ForEach语句会触发求值操作,而且流只能使用一次,接下来的行为我们就无法观察,如果我们还想继续调试下去,就需要重新创建一个流。

幸运的是,Java工程师们想到了这一点,为我们提供了 peek 方法。
这个方法可以让我们查看每个值,并且可以继续操作流

// 嗯,来段伪代码
***.stream()
  .peek(name -> System.out.println(name))
  .collection(***)
  .***

在 peek 方法中,我们可以使用打印方法来进行调试,也可以使用 IDE 的调试功能,还可以通过同样的方式将输出定向到日志系统中。

结语

到这,我们对于 Java 8 新特性函数式编程的系统性的学习就告一段落了。当然了,学习不能放下,坑还是要踩的,我会把未来日子中学到的新东西和新坑一点点的整理、总结。

在学习 Java 8 的函数式编程的过程中,我们用三篇 blog 来对这个阶段的学习进行一个总结性的描述:
* JDK1.8 中的 Lambda 表达式与 Stream API
* Stream API 中的高级集合类与收集器
* Java 8 函数式编程 —— 并行流

这有个二维码,不嫌弃的话,就关注下吧

【Java没基础】Java 8 并行流 ParallelStream_第2张图片

你可能感兴趣的:(Java基础修行,日常,Java,没基础系列)