您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

高效遍历数据,试试 Java8 中的 ParallelStream 并行流?

时间:2022-05-09 10:30:14  来源:CSDN  作者:实战Java

ParallelStream并行流在之前文章JAVA8新特性-Stream API中有简单的介绍过它的使用。如Collection集合可以通过parallelStream()的得到一个并行流。

Stream<Integer> stream = new ArrayList<Integer>().parallelStream();

串行流也可以通过parallel()方法转为并行流

Stream<Integer> stream = new ArrayList<Integer>().stream().parallel();

笔者在学习的过程中,也对并行流有着很多的疑问

  • 串行流和并行流哪个效率更高?(这还有疑问吗?肯定是并行流呀?sure?)
  • 并行流得到的结果是否一定准确?
  • 它的实现机制是什么样的?
  • 开发中可以使用并行流嘛?

现在就让我们来深入了解一下Java8的这个新特性——并行流

并行流的效率是否更高

在Java8以前,遍历一个长度非常大的集合往往非常麻烦,如需要使用多个线程配合synchronized,Lock和Atomic原子引用等进行遍历,且不说多线程之间的调度,多线程同步API的上手成本也比较高。

现在我们有更为简单的遍历方式,且不局限于遍历集合。

先往一个List添加10万条记录,代码比较简单,单条记录的内容使用UUID随机生成的英文字符串填充

List<String> list = new ArrayList<String>();
for (int i = 0; i < 100000; i++) {
    list.add(UUID.randomUUID().toString());
}

普通for循环该List,然后将每条记录中的a替换成b

for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
    String replace = s.replace("a", "b");
}

注意:这里使用String replace = s.replace("a", "b");这一行代码作为简单的业务处理,而不是System.out.println(s),因为打印的时候存在synchronized同步机制,会严重影响并行流的效率!

增强for循环

for (String s : list) {
    String replace = s.replace("a", "b");
}

串行流

list.stream().forEach((s)->{
    String replace = s.replace("a", "b");
});

并行流

list.parallelStream().forEach((s)->{
    String replace = s.replace("a", "b");
});

在保证执行机器一样的情况下,上述遍历代码各执行十次,取执行时间的平均值,单位毫秒,结果如下:

高效遍历数据,试试 Java8 中的 ParallelStream 并行流?

 

从结果中可知,在数据量较大的情况下,普通for,增强for和串行流的差距并不是很大,而并行流则以肉眼可见的差距领先于另外三者

数据量较大的情况下,并行流的遍历效率数倍于顺序遍历,在小数据量的情况下,并行流的效率还会那么高吗?

将上面10万的数据量改为1000,然后重复一百次取平均值,结果如下:

高效遍历数据,试试 Java8 中的 ParallelStream 并行流?

 

对结果进行分析,现在开发中比较少见的普通for遍历集合的方式,居然是顺序遍历中速度最快的!而它的改进版增强for速度小逊于普通for。

究其原因,是增强for内部使用迭代器进行遍历,需要维护ArrayList中的size变量,故而增加了时间开销。

而串行流的时间开销确实有点迷,可能的原因是开启流和关闭流的时间开销比较大

并行流花费的时间仍然优秀于另外的三种遍历方式!

不过,有一点需要注意的是,并行流在执行时,CPU的占用会比另外三者高

现在我们可以得到一个结论,并行流在大数据量时,对比其它的遍历方式有几倍的提升,而在数据量比较小时,提升不明显。

并行流处理结果是否准确

这个准确,举个例子来说,我希望遍历打印一个存有0 1 2 3 4 5 6 7 8 9的list,如0 1 2 3 4 5 6 7 8 9,代码可能会这么写

//数据
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    list.add(i);
}
//遍历打印
list.stream().forEach(i -> System.out.print(i + " "));

打印的结果如下:

0 1 2 3 4 5 6 7 8 9

结果没有任何问题,如果是并行流呢?遍历代码如下

list.parallelStream().forEach(i -> System.out.print(i + " "));

打印的结果如下:

6 5 1 0 9 3 7 8 2 4 

第二次打印的结果如下:

6 5 0 1 7 9 8 3 4 2 

可以看到打印出来的顺序是混乱无规律的

那是什么原因导致的呢?

并行流内部使用了默认的ForkJoinPool线程池,所以它默认的线程数量就是处理器的数量,通过Runtime.getRuntime().avAIlableProcessors()可以得到这个值。

笔者电脑的线程数是12,这意味着并行流最多可以将任务划分为12个小模块进行处理,然后再合并计算得到结果

如将0~9这是个数字进行划分:

0 1 2 3 4 5 6 7 8 9 
第一次划分得到两个小模块:
0 1 2 3 4  
5 6 7 8 9
第二次划分得到四个小模块:
0 1 2
3 4 
5 6 7
8 9
第三次划分得到八个小模块:
0 1 
2
3 
4
5 6
7
8
9
第三次划分时,2 3 4这些数据,明显已经不能再继续划分,故而2 3 4 这些数据可以先进行打印
第四次划分得到10个小模块:
0
1
2
3
4
5
6
7
8
9
这些小模块在无法继续细分后就会被打印,而打印处理的时候为了提高效率,不分先后顺序,故而造成打印的乱序

结合以上的测试数据,我们可以得到这样一个结论,当需要遍历的数据,存在强顺序性时,不能使用并行流,如顺序打印0~9;不要求顺序性时,可以使用并行流以提高效率,如将集合中的字符串中的"a"替换成"b"

并行流的实现机制

在Java7时,就已经提供了一个并发执行任务的API,Fork/Join,将一个大任务,拆分成若干个小任务, 再将各个小任务的运行结果汇总成最终的结果。

高效遍历数据,试试 Java8 中的 ParallelStream 并行流?

 

而在java8提供的并行流中,在实现Fork/Join的基础上,还用了工作窃取模式来获取各个小模块的运行结果,使之效率更高!这个知识点笔者后续会另外写一篇文章来介绍,敬请期待。

我们也可以使用Fock/Join机制,模仿一下并行流的实现过程。

如:进行数据的累加

public class ForkJionCalculate extends RecursiveTask<Long> {

    private long start;

    private long end;
 /**
  * 临界值
  */
    private static final long THRESHOLD = 10000L;

    public ForkJionCalculate(long start, long end) {
        this.start = start;
        this.end = end;
    }
    /**
     * 计算方法
     * @return
     */
    @Override
    protected Long compute() {
        long length = end - start;
        if (length <= THRESHOLD) {
            long sum = 0;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            long middle = (start + end) / 2;
            ForkJionCalculate left = new ForkJionCalculate(start, middle);
            left.fork();//拆分,并将该子任务压入线程队列
            ForkJionCalculate right = new ForkJionCalculate(middle + 1, end);
            right.fork();
            return left.join() + right.join();
        }
    }
}

处理类需要实现RecursiveTask<T>接口,还需指定一个临界值,临界值的作用就是指定将任务拆分到什么程度就不拆了

测试代码:

 public static void main(String[] args) {
     Instant start = Instant.now();
     ForkJoinPool pool = new ForkJoinPool();
     ForkJionCalculate task = new ForkJionCalculate(0, 10000000000L);
     Long sum = pool.invoke(task);
     System.out.println(sum);
     Instant end = Instant.now();
     System.out.println("耗费时间:" + Duration.between(start, end).toMillis());
 }

并行流的适用场景

其实Java这门编程语言其实有很多种用途,通过swing类库可以构建图形用户界面,配合ParallelGC进行一些科学计算任务,不过最广泛的用途,还是作为一门服务器语言,开发服务器应用,我们以这种方式进行测试。

我们使用SpringBoot构建一个工程,然后写一个控制器类,在控制器类中,如上进行1000和10万的数据量测试

另外使用PostMan发送1000并发调用该接口,取平均时间,单位为毫秒值

控制器类测试代码:

@RequestMApping("/parallel")
@ResponseBody
public String parallel() {
    //生成测试数据
    List<String> list = new ArrayList<>();
    for (int i = 0; i < 1000; i++) {
        list.add(UUID.randomUUID().toString());
    }
    //普通for遍历
    for (int i = 0; i < 1000; i++) {
        String s = list.get(i);
        String replace = s.replace("a", "b");
    }
    return "SUCCESS";
}

数据量1000时,每次请求消耗的时间

高效遍历数据,试试 Java8 中的 ParallelStream 并行流?

 

数据量10W时,每次请求消耗的时间

高效遍历数据,试试 Java8 中的 ParallelStream 并行流?

 

在之前的测试中,并行流对比其他的遍历方式都有两倍以上的差距,而在并发量较大的情况下,服务器线程本身就处于繁忙的状态,即使使用并行流,优化的空间也不是很大,而且CPU的占用率也会比较高。故而可以看到,并行流在数据量1000或者10万时,提升不是特别明显。

但是并不是说并行流不能用于平常的开发中,如CPU本身的负载不高的情况下,还是可以使用的;在一些定时任务的项目中,为了缩短定时任务的执行时间,也可以斟酌使用。

最后总结一下:在数据量比较大的情况下,CPU负载本身不是很高,不要求顺序执行的时候,可以使用并行流。

 

来源:
blog.csdn.NET/weixiang2039/article/details/107102364



Tags:遍历数据   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
高效遍历数据,试试 Java8 中的 ParallelStream 并行流?
ParallelStream并行流在之前文章Java8新特性-Stream API中有简单的介绍过它的使用。如Collection集合可以通过parallelStream()的得到一个并行流。Stream<Integer> stream =...【详细内容】
2022-05-09  Search: 遍历数据  点击:(416)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(18)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(25)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(30)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(61)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(77)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(78)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(97)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(111)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(105)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(82)  评论:(0)  加入收藏
相关文章
    无相关信息
站内最新
站内热门
站内头条