看到题目是不是有点疑问:你确定你没搞错?!数组求和???遍历一遍累加起来不就可以了吗???
是的,你说的都对,都听你的,但是我说的就是数组求和,并且我也确实是刚刚学会。╮(╯▽╰)╭
继续看下去吧,或许你的疑问会解开↓
注:记录于学习完《Java 8 实战》数据并行处理与性能,如果有错误,欢迎大佬指正
我相信你和我一样,提到数组求和,肯定最想想到的就是将数组迭代一遍,累加迭代元素。这是最简单的一种方式,代码实现如下:
public static long traditionSum(long[] arr){ //和 long sum = 0; //遍历数组中的每个元素 for (long l : arr) { //累加 sum += l; } return sum; }
为了便于我们测试性能,我们写一个比较通用的测试函数,用来记录对每种方式的运行时间,直接看代码吧!
public static long test(Function<long[], Long> function, long[] arr){ //记录最快的时间 long fasttime = Long.MAX_VALUE; //对函数调用10次 for (int i = 0; i < 10; i++) { //记录开始的系统时间 long start = System.nanoTime(); //执行函数 long result = function.apply(arr); //获取运行时间转换为ms long time = (System.nanoTime() - start) / 1_000_000; //打印本次的就和结果 System.out.println("结果为:" + result); //更新最快的时间 if (time < fasttime) { fasttime = time; } } return fasttime; }
方法有了,我们当然要准备好我们的测试数据了,为了简便起见,我们直接顺序生成1到100,000,000(1亿)来最为待求和的数组:
long[] longs = LongStream.rangeClosed(1, 100_000_000).toArray();
数据有了,我们可以测试一下传统方式的性能了(所在类TestArraysSum)
public static void main(String[] args) { long[] longs = LongStream.rangeClosed(1, 100_000_000).toArray(); //执行测试函数 long time = test(TestArraysSum::traditionSum, longs); System.out.println("时间为: " + time + "ms"); }
结果:
结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 时间为: 62ms
继续看其他方式
java 8的流可谓是非常的强大,配合lambda表达式和方法引用,极大的简化了对数据处理方面,下面是使用流对数组进行顺序求和
public static long sequentialSum(long[] arr){ return Arrays.stream(arr) .reduce(0L, Long::sum); }
Java 8让我们的代码极大的简化了,那么性能如何呢?
我们将main方法内执行求和方法部分换为调用这个方法看看
long time = test(TestArraysSum::sequentialSum, longs);
结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 时间为: 62ms
emmmm 好像差不多,Ծ‸Ծ,先不急,Java 8的流给我们带来的另一大好处还没用上呢,下面我们就来看看吧
Java 8 的Stream流可以让我们非常简单的去使用多线程解决问题,而我们的求和需求好像完美适合多线程问题去解决
public static long parallelSum(long[] arr){ return Arrays.stream(arr) .parallel() .reduce(0L, Long::sum); }
long time = test(TestArraysSum::parallelSum, longs);
结果
结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 时间为: 52ms
哦吼~这就很舒服了,是不是瞬间就快了
注:并行流内部默认使用ForkJoinPool的线程池,线程数量默认为计算机处理器的数量,使用Runtime.getRuntime().availableProcessors()可以获取处理器核心数
(我的测试环境是8个),可是设置这个值,但是只能全局设置,所以最好还是不要更改
是不是疑问我们除了调用parallel()方法以外什么都没干,究竟是怎么实现多线程的呢,其实并行流底层使用的是Java 7的分支/合并框架,下面我们就看一下使用分支/合并框架实现多线程求和吧!
分支合并框架的目的是以递归的方式将可以并行的任务拆分成更小的子任务,然后将每个子任务的结果进行合并生成整体结果。
我们可以继承RecursiveTask实现其compute()方法
分支合并实现的类ForkJoinSumCalculator
package java_8.sum; import java.util.concurrent.RecursiveTask; public class ForkJoinSumCalculator extends RecursiveTask<Long> { //任务处理的数组 private final long[] arr; //当前任务处理的开始和结束索引 private final int start; private final int end; //划分到处理数组的长度10_000_000变不来划分,进而合并 public static final long THRESHOLD = 10_000_000; //公共的构造函数,用来创建主任务 public ForkJoinSumCalculator(long[] arr){ this(arr,0,arr.length); } //私有的构造函数,用来创建子任务 private ForkJoinSumCalculator(long[] arr, int start, int end){ this.arr = arr; this.start = start; this.end = end; } //实现的方法 @Override protected Long compute() { //当时子任务处理长度 int length = end - start; //当数组处理长度足够小时 if (length <= THRESHOLD){ //进行合并 return computeSequentially(); } //创建第1个子任务对前面一半数组进行求和 ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(arr, start, start + length / 2); //使用线程池中的另一个线程求和前一半 leftTask.fork(); //创建第2个子任务对后一半数组进行求和 ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(arr, start + length / 2, end); //直接使用当前线程进行求和 获取求和结果 Long rightResult = rightTask.compute(); //获取前一半的求和结果 Long leftTesult = leftTask.join(); //合并 return leftTesult + rightResult; } //合并是的调用方法 迭代求和 private long computeSequentially(){ long sum = 0; for (int i = start; i < end; i++) { sum += arr[i]; } return sum; } }
public static final long THRESHOLD = 10_000_000;
划分的界线使我随便设定的当前值的情况下会划分为10个线程
然后我们就可以编写我们的求和方法了
public static long forkJoinSum(long[] arr){ ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(arr); return new ForkJoinPool().invoke(calculator); }
long time = test(TestArraysSum::forkJoinSum, longs);
结果:
结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 结果为:5000000050000000 时间为: 53ms
还不错,跟并行流的性能差不多
由于分支合并时的递归调用也消耗性能,因此我们更改public static final long THRESHOLD = 10_000_000;的大小时,运行时间会差距很大。
具体更改多少效率最高,这个真的不好说