1 并行流
1.1 声明式编程
通过前面的学习我们知道,Java8Stream的接口可以实现声明式处理数据,而不必考虑细节处理。
前面我们一直在接触的是“流”的思想,而且大多是流水线式的单线程处理。
现在考虑在Java8中,是如何进行多线程操作的。
1.2 并行数据处理
在Java7之前。并行处理数据基本都是通过开辟多线程来解决的,具体流程如下。
- 将数据分成部分
- 给每个子部分分配一个子线程
- 等待所有的子线程全部结束
- 合并子线程
这样的并行数据处理不稳定、易出错。
1.3 并行流
在Java8中,Stream接口应用分支/合并框架,将一个数据内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
2 测试并行流的性能
2.1 将顺序流转换为并行流
1.问题情景
对前n个自然数求和。
2.传统Java顺序流
代码设计:
public void orderSum(long n) {
long result = 0;
for (long i = 1L; i <= n; i++) {
result += i;
}
System.out.println("---顺序流运行结果 sum的值---\r\n" + result);
}
3.Java8并行流
代码设计:
public void parallelSum(long n) {
long result = Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel()
.reduce(0L, Long::sum);
System.out.println("---并行流运行结果 sum的值---\r\n" + result);
}
原理解析
与传统的求和累加方式不同之处在于,Stream在内部将数据流分成了几块不同的数据块,因此可以单独对不同的数据块并行进行求和归纳操作。最后,将各个子流的求和归纳结果合并起来,就得到整个原始流的归纳结果。
配置并行流使用的线程池
并行流内部使用了默认的ForkJoinPool,它默认的线程数量就是你的处理器数量,这个值是由Runtime.getRuntime().availableProcessors()得到的。
也可以通过系统属性java.util.concurrent.ForkJoinPool.common.parallelism来改变线程池大小的,如下所示:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
2.2 测量并行流的性能
下面测试顺序、迭代以及并行流三种方法的性能。
首先写出性能测试函数,代码设计:
1.性能测试函数
public static long meaureParallelSumTest(Function<Long, Long> adder, long n) {
long fastestTime = Long.MAX_VALUE;//将fastestTime设为long.MAX_VALUE可以保证所有duration都不会比fsatestTime大
for (int i = 0; i < 10; i++) {
long startTime = System.nanoTime();//开始时间
long sum = adder.apply(n);
long duration = (System.nanoTime() - startTime) / 1000000;
if (duration < fastestTime) {
fastestTime = duration;//记录最短的一次
}
}
return fastestTime;
}
}
这个方法接受一个函数和一个long作为参数。
它会对传给方法的long应用函数10次,记录每次执行的时间,单位为毫秒,并返回最短的一次。
2.顺序流
代码设计:
System.out.println("---并行流的最快时间---\r\n" + meaureParallelSumTest(ParallelSumTest::parallelSumTest, 10000000) + "msecs");
运行一千万次的时间:
---顺序流的最快时间---
3msecs
3.并行流
代码设计:
System.out.println("---顺序流的最快时间---\r\n" + meaureParallelSumTest(ParallelSumTest::orderSumTest, 10000000) + "msecs");
运行一千万次的时间:
---并行流的最快时间---
331msecs
4.分析
由上面的测试结果可以看到,顺序流的运行时间远远快与并行流,似乎有些出乎意料。
这是因为:
传统for循环的迭代版本更为底层,更重要的是不需要对原始类型做任何装箱或拆箱操作。
而并行流的运行是这样的:
- iterate生成的是装箱的对象,必须不断的装箱、拆箱才能进行数字求和;
- 我们很难把iterate分成多个独立块来并行执行。