前言

看到题目是不是有点疑问:你确定你没搞错?!数组求和???遍历一遍累加起来不就可以了吗???

是的,你说的都对,都听你的,但是我说的就是数组求和,并且我也确实是刚刚学会。╮(╯▽╰)╭

继续看下去吧,或许你的疑问会解开↓

注:记录于学习完《Java 8 实战》数据并行处理与性能,如果有错误,欢迎大佬指正

传统方式

求和方法

我相信你和我一样,提到数组求和,肯定最想想到的就是将数组迭代一遍,累加迭代元素。这是最简单的一种方式,代码实现如下:

  1. public static long traditionSum(long[] arr){
  2. //和
  3. long sum = 0;
  4. //遍历数组中的每个元素
  5. for (long l : arr) {
  6. //累加
  7. sum += l;
  8. }
  9. return sum;
  10. }

性能测试方法

为了便于我们测试性能,我们写一个比较通用的测试函数,用来记录对每种方式的运行时间,直接看代码吧!

  1. public static long test(Function<long[], Long> function, long[] arr){
  2. //记录最快的时间
  3. long fasttime = Long.MAX_VALUE;
  4. //对函数调用10次
  5. for (int i = 0; i < 10; i++) {
  6. //记录开始的系统时间
  7. long start = System.nanoTime();
  8. //执行函数
  9. long result = function.apply(arr);
  10. //获取运行时间转换为ms
  11. long time = (System.nanoTime() - start) / 1_000_000;
  12. //打印本次的就和结果
  13. System.out.println("结果为:" + result);
  14. //更新最快的时间
  15. if (time < fasttime) {
  16. fasttime = time;
  17. }
  18. }
  19. return fasttime;
  20. }

性能测试代码解释

  • 传入参数Function<long[], Long> function: 我们需要测试的函数,稍后我们会把每种求和方式都传入到这个参数里面。如果你对java 8的新特性(Lambda表达式、行为参数化、方法引用等)不熟悉,那么你可以理解为Function是一个匿名类,我们传入的求和方法会放到function.apply()的方法中,我们调用apply()方法,实际上就是调用我们传入的求和方法。
  • Function<long[], Long>的泛型: 第一个为我们求和方法需要传入的参数的类型(传入一个long类型的数组作为待求和数组),第二个为我们的求和方法返回值的类型(返回数组的和为long)
  • long[] arr:待求和数组
  • 关于为什么会调用10次:任何的Java代码都需要多执行几次才会被JIT编译器优化,多执行几次是为了保证我们测量性能的准确性。

数据准备

方法有了,我们当然要准备好我们的测试数据了,为了简便起见,我们直接顺序生成1到100,000,000(1亿)来最为待求和的数组:

  1. long[] longs = LongStream.rangeClosed(1, 100_000_000).toArray();

测试性能

数据有了,我们可以测试一下传统方式的性能了(所在类TestArraysSum)

  1. public static void main(String[] args) {
  2. long[] longs = LongStream.rangeClosed(1, 100_000_000).toArray();
  3. //执行测试函数
  4. long time = test(TestArraysSum::traditionSum, longs);
  5. System.out.println("时间为: " + time + "ms");
  6. }

结果:

  1. 结果为:5000000050000000
  2. 结果为:5000000050000000
  3. 结果为:5000000050000000
  4. 结果为:5000000050000000
  5. 结果为:5000000050000000
  6. 结果为:5000000050000000
  7. 结果为:5000000050000000
  8. 结果为:5000000050000000
  9. 结果为:5000000050000000
  10. 结果为:5000000050000000
  11. 时间为: 62ms

继续看其他方式

Stream流的顺序执行方式

求和方法

java 8的流可谓是非常的强大,配合lambda表达式和方法引用,极大的简化了对数据处理方面,下面是使用流对数组进行顺序求和

  1. public static long sequentialSum(long[] arr){
  2. return Arrays.stream(arr)
  3. .reduce(0L, Long::sum);
  4. }

代码解释

  • Arrays.stream(arr)将我们传入的数组变为一个流(此处没有Java包装类与原始类型的装箱和拆箱,装箱和拆箱会极大影响性能,应该尽量避免)
  • .reduce(0L, Long::sum):0L是初始值,Long::sum通过方法引用的方式使用Long提供的求和函数,对数组的每一个元素都进行求和

性能测试

Java 8让我们的代码极大的简化了,那么性能如何呢?

我们将main方法内执行求和方法部分换为调用这个方法看看

  1. long time = test(TestArraysSum::sequentialSum, longs);
  1. 结果为:5000000050000000
  2. 结果为:5000000050000000
  3. 结果为:5000000050000000
  4. 结果为:5000000050000000
  5. 结果为:5000000050000000
  6. 结果为:5000000050000000
  7. 结果为:5000000050000000
  8. 结果为:5000000050000000
  9. 结果为:5000000050000000
  10. 结果为:5000000050000000
  11. 时间为: 62ms

emmmm 好像差不多,Ծ‸Ծ,先不急,Java 8的流给我们带来的另一大好处还没用上呢,下面我们就来看看吧

Stream流的并行执行

求和方法

Java 8 的Stream流可以让我们非常简单的去使用多线程解决问题,而我们的求和需求好像完美适合多线程问题去解决

  1. public static long parallelSum(long[] arr){
  2. return Arrays.stream(arr)
  3. .parallel()
  4. .reduce(0L, Long::sum);
  5. }

代码解释

  • .parallel():与顺序流实现相比,仅仅是多调用了一个parallel()方法,他的作用就是将顺序流转化为并行流(其实就是改变了一下boolean标志),如何并行执行呢,不用我们实现,无脑调用就好了

性能测试

  1. long time = test(TestArraysSum::parallelSum, longs);

结果

  1. 结果为:5000000050000000
  2. 结果为:5000000050000000
  3. 结果为:5000000050000000
  4. 结果为:5000000050000000
  5. 结果为:5000000050000000
  6. 结果为:5000000050000000
  7. 结果为:5000000050000000
  8. 结果为:5000000050000000
  9. 结果为:5000000050000000
  10. 结果为:5000000050000000
  11. 时间为: 52ms

哦吼~这就很舒服了,是不是瞬间就快了

注:并行流内部默认使用ForkJoinPool的线程池,线程数量默认为计算机处理器的数量,使用Runtime.getRuntime().availableProcessors()可以获取处理器核心数

(我的测试环境是8个),可是设置这个值,但是只能全局设置,所以最好还是不要更改

是不是疑问我们除了调用parallel()方法以外什么都没干,究竟是怎么实现多线程的呢,其实并行流底层使用的是Java 7的分支/合并框架,下面我们就看一下使用分支/合并框架实现多线程求和吧!

分支合并框架的实现方式

分支合并框架的目的是以递归的方式将可以并行的任务拆分成更小的子任务,然后将每个子任务的结果进行合并生成整体结果。

求和方法

我们可以继承RecursiveTask实现其compute()方法

分支合并实现的类ForkJoinSumCalculator

  1. package java_8.sum;
  2. import java.util.concurrent.RecursiveTask;
  3. public class ForkJoinSumCalculator extends RecursiveTask<Long> {
  4. //任务处理的数组
  5. private final long[] arr;
  6. //当前任务处理的开始和结束索引
  7. private final int start;
  8. private final int end;
  9. //划分到处理数组的长度10_000_000变不来划分,进而合并
  10. public static final long THRESHOLD = 10_000_000;
  11. //公共的构造函数,用来创建主任务
  12. public ForkJoinSumCalculator(long[] arr){
  13. this(arr,0,arr.length);
  14. }
  15. //私有的构造函数,用来创建子任务
  16. private ForkJoinSumCalculator(long[] arr, int start, int end){
  17. this.arr = arr;
  18. this.start = start;
  19. this.end = end;
  20. }
  21. //实现的方法
  22. @Override
  23. protected Long compute() {
  24. //当时子任务处理长度
  25. int length = end - start;
  26. //当数组处理长度足够小时
  27. if (length <= THRESHOLD){
  28. //进行合并
  29. return computeSequentially();
  30. }
  31. //创建第1个子任务对前面一半数组进行求和
  32. ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(arr, start, start + length / 2);
  33. //使用线程池中的另一个线程求和前一半
  34. leftTask.fork();
  35. //创建第2个子任务对后一半数组进行求和
  36. ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(arr, start + length / 2, end);
  37. //直接使用当前线程进行求和 获取求和结果
  38. Long rightResult = rightTask.compute();
  39. //获取前一半的求和结果
  40. Long leftTesult = leftTask.join();
  41. //合并
  42. return leftTesult + rightResult;
  43. }
  44. //合并是的调用方法 迭代求和
  45. private long computeSequentially(){
  46. long sum = 0;
  47. for (int i = start; i < end; i++) {
  48. sum += arr[i];
  49. }
  50. return sum;
  51. }
  52. }

public static final long THRESHOLD = 10_000_000;

划分的界线使我随便设定的当前值的情况下会划分为10个线程

然后我们就可以编写我们的求和方法了

  1. public static long forkJoinSum(long[] arr){
  2. ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(arr);
  3. return new ForkJoinPool().invoke(calculator);
  4. }

性能测试

  1. long time = test(TestArraysSum::forkJoinSum, longs);

结果:

  1. 结果为:5000000050000000
  2. 结果为:5000000050000000
  3. 结果为:5000000050000000
  4. 结果为:5000000050000000
  5. 结果为:5000000050000000
  6. 结果为:5000000050000000
  7. 结果为:5000000050000000
  8. 结果为:5000000050000000
  9. 结果为:5000000050000000
  10. 结果为:5000000050000000
  11. 时间为: 53ms

还不错,跟并行流的性能差不多

由于分支合并时的递归调用也消耗性能,因此我们更改public static final long THRESHOLD = 10_000_000;的大小时,运行时间会差距很大。

具体更改多少效率最高,这个真的不好说

总结

  • 使用了4种方式完成数组求和
  • 使用传统方式(遍历)效率其实也不低,因为实现方式比较接近底层
  • 使用流极大简化了数组处理
  • 并行流在适合的场景下可以大展身手
  • 并行流使用分支合并框架实现

感动,我终于学会了Java对数组求和的更多相关文章

  1. Java之数组篇

    动手动脑,第六次Tutorial--数组 这次的Tutorial讲解了Java中如何进行数组操作,包括数组声明创建使用和赋值运算,写这篇文章的目的就是通过实际运用已达到对数组使用的更加熟练,下面是实践 ...

  2. java中数组的相关知识

      1. 2.数组的命名方法 1)int[]ages=new int[5]; 2) int[]ages; ages=new int[5]; 3)int[]ags={1,2,3,4,5}; 4)int[ ...

  3. Java基础——数组应用之StringBuilder类和StringBuffer类

    接上文:Java基础——数组应用之字符串String类 一.StringBuffer类 StringBuffer类和String一样,也用来代表字符串,只是由于StringBuffer的内部实现方式和 ...

  4. JSon_零基础_007_将JSon格式的"数组"字符串转换为Java对象"数组"

    将JSon格式的"数组"字符串转换为Java对象"数组". 应用此技术从一个json对象字符串格式中得到一个java对应的对象. JSONObject是一个“n ...

  5. java将数组中的零放到末尾

    package com.shb.java; /** * 将数组中的0放到数组的后边,然后原来的非零数的顺序不改变 * @author BIN * */ public class Demo2{ publ ...

  6. Java中数组的特性

    转载:http://blog.csdn.net/zhangjg_blog/article/details/16116613 数组是基本上所有语言都会有的一种数据类型,它表示一组相同类型的数据的集合,具 ...

  7. 在java 中,数组与 List<T> 类型的相互转换

    在java中,数组与List<T> 之前进行互相转换,转换方法可总结为以下几种: 一. 将 数组转换成List<T> 1. 使用 Collections 的addAll 方法 ...

  8. Java RGB数组图像合成 ImageCombining (整理)

    /** * Java RGB数组图像合成 ImageCombinning (整理) * * 2016-1-2 深圳 南山平山村 曾剑锋 * * 注意事项: * 1.本程序为java程序,同时感谢您花费 ...

  9. java对象数组

    问题描述:     java 对象数组的使用 问题解决: 数组元素可以是任何类型(只要所有元素具有相同的类型) 数组元素可以是基本数据类型 数组元素也可以是类对象,称这样的数组为对象数组.在这种情况下 ...

随机推荐

  1. office一直提示配置进度与图标问题

    原来安装了wps office,广告太烦,于是卸载了wps安装Microsoft office 2010,安装完成后每次打开excel文件都要重新安装配置,修改注册表norereg和设置兼容模式都不行 ...

  2. Vue2.0 【第三季】第2节 computed Option 计算选项

    目录 Vue2.0 [第三季]第2节 computed Option 计算选项 第2节 computed Option 计算选项 一.格式化输出结果 二.用计算属性反转数组 Vue2.0 [第三季]第 ...

  3. BrowserSync(保存代码后,自动刷新浏览器)

    摘要 Browsersync能让浏览器实时.快速响应您的文件更改(html.js.css.sass.less等)并自动刷新页面.更重要的是 Browsersync可以同时在PC.平板.手机等设备下进项 ...

  4. 记一次修改mysql密码

    因为马大哈的原因,没有记录自己服务器mysql的密码,试来试去试不出来只好选择重置密码. 直接上命令: 首先 vim /etc/my.cnf 在my.cnf文件中,在[mysqld]的段中加上 ski ...

  5. 微信小程序开发(二)认识开发工具

    腾讯微信团队提供非常优秀的微信小程序开发工具,大大降低了开发者的入门门槛,为他们点赞!上一篇文章已经说明了,如何注册及下载开发工具,现在我们就来一起认识见识一下开发工具的庐山真面目. 首次打开这个开发 ...

  6. Flask 之分析线程和协程

    目录 flask之分析线程和协程 01 思考:每个请求之间的关系 02 threading.local 03 通过字典自定义threading.local 04 通过setattr和getattr实现 ...

  7. maven中的pom配置文件一——spring,mybatis,oracle,jstl,json,文件上传

    <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://mave ...

  8. docker系列详解<二>之常用命令

    此篇我们以从docker运行一个tomcat为例,进行一下操作: 拉取镜像 查看镜像 创建容器 查看运行状态 进入退出容器 停止容器 重启容器 删除容器 删除镜像 1.拉取tomcat镜像: 1).查 ...

  9. 遍历集合的常见方式,排序,用lambda表示是怎样的

       Collection集合的功能:            Object[] toArray() 将集合转成数组            Iterator iterator() 通过方法的调用 获取I ...

  10. 为什么 select count(*) from t,在 InnoDB 引擎中比 MyISAM 慢?

    统计一张表的总数量,是我们开发中常有的业务需求,通常情况下,我们都是使用 select count(*) from t SQL 语句来完成.随着业务数据的增加,你会发现这条语句执行的速度越来越慢,为什 ...