影响并行流性能的主要因素有5 个,依次分析如下。
- 数据大小
- 源数据结构
- 装箱
- 核的数量
- 单元处理开销
比如数据大小,这是一场并行执行花费时间和分解合并操作开销之间的战争。花在流中每个元素身上的时间越长,并行操作带来的性能提升越明显。
来看一个具体的问题,看看如何分解和合并它。如下是很简单的并行求和的代码。
private static int addIntegers(List<Integer> values) {
return values.parallelStream()
.mapToInt(i -> i)
.sum();
}
在底层,并行流还是沿用了fork/join 框架。fork 递归式地分解问题,然后每段并行执行,最终由join 合并结果,返回最后的值。
下图形象地展示了上面代码所示的操作。
使用fork/join 分解合并问题
1. 数据被分成四块。
2. 如代码所示,计算工作在每个线程里并行执行。这包括将每个Integer 对象映射为int值,然后在每个线程里将1/4 的数字相加。理想情况下,我们希望在这里花的时间越多越好,因为这里是并行操作的最佳场所。
3. 然后合并结果。在上面代码中,就是sum 操作,但这也可能是reduce、collect 或其他终结操作。
我们可以根据性能的好坏,将核心类库提供的通用数据结构分成以下3 组。
- 性能好
- 性能一般
- 性能差
初始的数据结构影响巨大。举一个极端的例子,对比对10 000 个整数并行求和,使用ArrayList要比使用LinkedList 快10 倍。这不是说业务逻辑的性能情况也会如此,只是说明了数据结构对于性能的影响之大。使用形如LinkedList 这样难于分解的数据结构并行运行可能更慢。
理想情况下,一旦流框架将问题分解成小块,就可以在每个线程里单独处理每一小块,线程之间不再需要进一步通信。无奈现实不总遂人愿!
如果能避开有状态,选用无状态操作,就能获得更好的并行性能。无状态操作包括map、filter 和flatMap,有状态操作包括sorted、distinct 和limit。