「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
这样的支持随机读取的结构,能够轻易的分解。
- 性能一般
HashSet
、TreeSet
这样的数据结构不易公平的分解。
- 性能差
有的数据结构难于分解,有的结构可能需要花O(N)
的时间复杂度来分解。比如:LinkedList
,难以对半分解;Streams.iterate
和BufferedRead.lines
这样长度未知的数据结构也难以分解。
操作的状态
流中的操作,可以分为有状态和无状态。无状态的操作在整个操作中不必维护状态;有状态的操作则有维护状态所需的开销和限制。
避开有状态的操作,可以获得更好的并行性能。无状态的操作包括map
、filter
和flatMap
;有状态的操作包括sorted
、distinct
和limit
。
并行化数组操作
Java 8
为数组提供了并行化操作的方法,这些方法在Arrays
类中:
method | description |
---|---|
parallelPrefix | 任意给定一个函数,计算数组的“和”(任意BinaryOperator ) |
parallelSetAll | 使用Lambda 表达式更新数组元素 |
parallelSort | 并行化对数组元素排序 |