「Java 8 函数式编程」读书笔记——数据并行化

本书第六章的读书笔记,也是我这个系列的最后一篇读书笔记。后面7、8、9章分别讲的“测试、调试与重构”、“设计和架构的原则”以及“使用Lambda表达式编写并发程序”,因为笔记不好整理,就不写了,感兴趣的同学自己买书来看吧。

并行化流操作

关于并行并发的区别和并行的重要性的讨论这里不做笔记了,直接看Stream类库提供了哪些关于并行的操作把。

  • 如果已经有了一个Stream对象,可以调用parallel方法使其拥有并行操作的能力;
  • 如果想从一个集合类创建一个Stream对象,可以调用parallelStream方法获得一个拥有并行能力的流。
  • BaseStream提供的sequential方法将会按顺序遍历Stream中的元素

对于同一个Stream对象,如果parallel方法和sequential方法都被调用,最后调用的那个方法将起效,不能同时处于两种模式。

使用并行流,不需要写代码用于处理调度和等待线程池中的某项任务完成,这些工作都交由类库完成。

限制

编写并行流,存在一些与非并行流不一样的约定。

reduce方法的限制

初值必须为组合函数的恒等值

使用恒等值与其他值做reduce运算时,其他值保持不变。比如,使用reduce进行求和运算时,初值必须为0,而进行求积运算时,初值必须为1

reduce操作必须符合结合律

因为并行计算时,元素的遍历顺序是不确定的,所以只有符合结合律才能保证结果是确定的。

避免持有锁

前面提到过,并行流的操作,是把线程的调度等工作交给了类库解决的,所以不要做持有锁的操作,否则是自找麻烦

性能

主要影响因素

影响并行流的性能的因素主要有5个:

  • 数据大小

因为并行处理会带来分解数据和合并数据的额外开销,所以只有当数据量足够大时使用并行流操作才具有意义,否则就是在浪费资源。

  • 源数据结构

源数据通常是集合,而因为具体的类型不同,造成了分割时的开销不同。

  • 装箱

基本数据类型比装箱类型处理更快。

  • 核的数量

拥有的核数量越多,潜在的性能提升越大。但这里的核是指运行时进程能够使用的核的数量。

  • 单元处理开销

花在每个元素上的处理时间越长,并行带来的性能提升越大。

底层框架

并行流在底层沿用的fork/join框架,fork递归式的分解问题,然后每段并行执行,最终由join合并结果,返回最后的值。

数据结构分解的难易

数据结构对半分解的难易程度,决定了分解的效率。可以将核心类库提供的通用数据结构分为三类:

  • 性能好

ArrayList数组或者IntStream.range这样的支持随机读取的结构,能够轻易的分解。

  • 性能一般

HashSetTreeSet这样的数据结构不易公平的分解。

  • 性能差

有的数据结构难于分解,有的结构可能需要花O(N)的时间复杂度来分解。比如:LinkedList,难以对半分解;Streams.iterateBufferedRead.lines这样长度未知的数据结构也难以分解。

操作的状态

流中的操作,可以分为有状态无状态。无状态的操作在整个操作中不必维护状态;有状态的操作则有维护状态所需的开销和限制。

避开有状态的操作,可以获得更好的并行性能。无状态的操作包括mapfilterflatMap;有状态的操作包括sorteddistinctlimit

并行化数组操作

Java 8为数组提供了并行化操作的方法,这些方法在Arrays类中:

method description
parallelPrefix 任意给定一个函数,计算数组的“和”(任意BinaryOperator
parallelSetAll 使用Lambda表达式更新数组元素
parallelSort 并行化对数组元素排序