在JDK7之前,并行处理数据集合非常麻烦。首先需要自己明确的把包含数据的数据结构分成若干个子部分,第二需要给每个子部分分配一个独立的线程;第三需要在恰当的时候对它们进行同步来避免不希望出现的竞争条件,等待所有线程完成,最后把这些部分合并起来。
Doug Lea 在JDK7中引入了fork/join框架,让这些操作更稳定,更不易出错。
本节主要内容:
1. 用并行流并行处理数据
2. 并行流的性能分析
3. fork/join框架
4. 使用Spliterator分割流
学完本节期望能达到:
1. 熟练使用并行流,来加速业务性能
2. 了解流内部的工作原理,以防止误用的情况
3. 通过Spliterator控制数据块的划分方式
并行流
可以通过对数据源调用parallelStream方法来将源转换为并行流。并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。这样可以自动将工作负荷转到多核中并行处理。
考虑下面一个实现:给定正整数n,计算 1 + 2 + … n的和。
使用stream的实现:
private static long sequentialSum(long n) {
return Stream.iterate(1L, i -> i + 1).limit(n).reduce(0L, Long::sum);
}
将上面的顺序流转换为并行流,实现如下:
private static long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, Long::sum);
}
即通过调用方法parallel可将顺序流转换为并行流。
但需要注意的是流仅在终端操作时才开始执行,所以当前流是顺序流还是并行流以最靠近终端操作的流类型为准,示例:
list.stream().parallel().filter(e -> e.age > 20).sequential().map(...).parallel().collect(...);
此种情况并不会按预想的先使用并行流执行过滤,再按顺序流执行映射转换。而是整个流水线操作都按并行流执行。
配置并行流使用的线程池
并行流内部使用了默认的ForkJoinPool, 它默认的线程数量就是处理器的数量(Runtime.getRuntime().availableProcessors())。也可以通过设置系统属性来改变它(System.setProperty(“java.util.concurrent.ForkJoinPool.common.parallelism”, “12”))。但它是一个全局设置,会影响所有的并行流,一般而言线程数等于处理器数量是一个合理的数值,不需要修改。
测试流性能
一般而言,同一个功能给我们的感觉是并行流性能会比顺序流性能更好。然而在软件工程中,优化性能的黄金准则是:测量。我们开发了程序,用来测量4种写法的累加,看看性能如何:
@Slf4j
public class SumSample {
/**
* 顺序流、并行流性能测试
* 实现1~1亿整型数字累加
*
*/
public static void main(String[] args) {
CostUtil.cost(() -> log.info("==> for: 1 + ... + 100_000_000, result: {}", forSum(100_000_000)));
log.info("================================================================================");
CostUtil.cost