Java 8实战-StreamVS传统for-each性能(续)

本文对比了Java8中Stream API与传统for-each循环在不同场景下的性能表现,尤其是在大量数据处理时,分析了并行流与顺序流的适用情况及性能影响因素。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上篇 https://blog.youkuaiyun.com/zangdaiyang1991/article/details/83755306 《Java 8实战阅读-StreamVS传统for-each性能》 

对"字符串遍历"这一常见场景进行简要分析,得到结论:

从性能角度考虑,遍历小数据量的List/Set这种一般场景不适合使用流式处理

------------------------------------------------------------------------------------------------------------------------------------------------------------

本篇结合《Java 8 实战》一书中建议,对适合采用流式处理(并行流)的一种场景做出简要验证,并列举书中作者对

流操作中选用并行流还是有序流的几个建议:

 1、如果有疑问,测量(PS:性能问题,实践永远是第一位的)。把顺序流转成并行流轻而易举,但却不一定是好事。我们在本节中已经指出,并行流并不总是比顺序流快。此外,并行流有时候会和你的直觉不一致,所
以在考虑选择顺序流还是并行流时,第一个也是最重要的建议就是用适当的基准来检查
其性能。
   2、留意装箱。自动装箱和拆箱操作会大大降低性能。Java 8中有原始类型流(IntStream、
LongStream、DoubleStream)来避免这种操作,但凡有可能都应该用这些流。
   3、有些操作本身在并行流上的性能就比顺序流差。特别是limit和findFirst等依赖于元
素顺序的操作,它们在并行流上执行的代价非常大。例如,findAny会比findFirst性
能好,因为它不一定要按顺序来执行。你总是可以调用unordered方法来把有序流变成
无序流。那么,如果你需要流中的n个元素而不是专门要前n个的话,对无序并行流调用
limit可能会比单个有序流(比如数据源是一个List)更高效。
   4、还要考虑流的操作流水线的总计算成本。设N是要处理的元素的总数,Q是一个元素通过
流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值较高就意味
着使用并行流时性能好的可能性比较大。
   5、对于较小的数据量,选择并行流几乎从来都不是一个好的决定。并行处理少数几个元素
的好处还抵不上并行化造成的额外开销。
   6、要考虑流背后的数据结构是否易于分解。例如,ArrayList的拆分效率比LinkedList
高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。另外,用range工厂方法创建的原始类型流也可以快速分解
     7、流自身的特点,以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。
例如,一个SIZED流可以分成大小相等的两部分,这样每个部分都可以比较高效地并行处
理,但筛选操作可能丢弃的元素个数却无法预测,导致流本身的大小未知
     8、还要考虑终端操作中合并步骤的代价是大是小(例如Collector中的combiner方法)。
如果这一步代价很大,那么组合每个子流产生的部分结果所付出的代价就可能会超出通
过并行流得到的性能提升

流的几种数据源可分解性评估(可并行)

表7-1 流的数据源和可分解性(摘自《Java 8 实战》)
源                             可分解性
ArrayList                  极佳
LinkedList                差
IntStream.range       极佳
Stream.iterate          差
HashSet                   好
TreeSet                    好

 

以下采用16核CPU

验证一:3000W字符串的HashSet流式处理场景(三个处理条件意味着流式处理要遍历三次数据)(ps:鉴于本机配置有限,只能测试该数据量级的)时的并行流性能)

验证二:LongStream.range(10亿数据量)这一适合并行流的场景

进行实际验证

结论:

1、性能优劣(验证一):for-each > 字符串并行流 (如果该场景修改为只有一级过滤条件,差距会大大减小,因为流式处理也只需要并行遍历一次数据了)

2、性能优劣(验证二):LongStream.range并行流性能 > for-each > 有序流

实践代码:

package com.csdn.test;

import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
import java.util.stream.Stream;

public class TestStreamSpeed
{

    /**
     * 流操作中选用并行流还是有序流的几个建议:
     * 
     *1、如果有疑问,测量。把顺序流转成并行流轻而易举,但却不一定是好事。我们在本节中
已经指出,并行流并不总是比顺序流快。此外,并行流有时候会和你的直觉不一致,所
以在考虑选择顺序流还是并行流时,第一个也是最重要的建议就是用适当的基准来检查
其性能。
   2、留意装箱。自动装箱和拆箱操作会大大降低性能。Java 8中有原始类型流(IntStream、
LongStream、DoubleStream)来避免这种操作,但凡有可能都应该用这些流。
   3、有些操作本身在并行流上的性能就比顺序流差。特别是limit和findFirst等依赖于元
素顺序的操作,它们在并行流上执行的代价非常大。例如,findAny会比findFirst性
能好,因为它不一定要按顺序来执行。你总是可以调用unordered方法来把有序流变成
无序流。那么,如果你需要流中的n个元素而不是专门要前n个的话,对无序并行流调用
limit可能会比单个有序流(比如数据源是一个List)更高效。
   4、还要考虑流的操作流水线的总计算成本。设N是要处理的元素的总数,Q是一个元素通过
流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值较高就意味
着使用并行流时性能好的可能性比较大。
   5、对于较小的数据量,选择并行流几乎从来都不是一个好的决定。并行处理少数几个元素
的好处还抵不上并行化造成的额外开销。
   6、要考虑流背后的数据结构是否易于分解。例如,ArrayList的拆分效率比LinkedList
高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。另外,用range工厂方法创建的原始类型流也可以快速分解
     7、流自身的特点,以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。
例如,一个SIZED流可以分成大小相等的两部分,这样每个部分都可以比较高效地并行处
理,但筛选操作可能丢弃的元素个数却无法预测,导致流本身的大小未知
     8、还要考虑终端操作中合并步骤的代价是大是小(例如Collector中的combiner方法)。
如果这一步代价很大,那么组合每个子流产生的部分结果所付出的代价就可能会超出通
过并行流得到的性能提升
     * 
     */
    
    
    // 流操作性能与环境 强相关
    // 必须经过验证 才能确定并行、还是有序流策略
    public static void testSumSpeed(long max)
    {
        int ms_ = 1_000_000;
        
        long start = System.nanoTime();
        long sum = LongStream.rangeClosed(0, max).parallel().sum();// 并行parallel
        System.out.println(String.format("sum value --> %s, IntStream sum done in --> %s", sum, (System.nanoTime() - start)/ms_));
        
        long start1 = System.nanoTime();
        long sum1 = LongStream.rangeClosed(0, max).parallel().sequential().sum();// 并行parallel/有序流sequential
        System.out.println(String.format("sum1 value --> %s, IntStream sum1 done in --> %s", sum1, (System.nanoTime() - start1)/ms_));
        
        long start_ = System.nanoTime();
        long sum_ = 0l;
        for (long i = 0; i <= max; i++)
        {
            sum_ += i;
        }
        System.out.println(String.format("sum_ value --> %s, foreach sum done in --> %s", sum_, (System.nanoTime() - start_)/ms_));
    }
    
    
    
    public static void main(String[] args)
    {
        Set<String> aSet = new HashSet<String>();
        for (int i = 0; i < 30000000; i++)
        {
            aSet.add(i % 2 == 0 ? String.valueOf(i) : String.valueOf(i) + " ");
        }
        // 并行流内部使用了默认的ForkJoinPool(分支/合并框架),它默认的线程数量就是你的处理器数量Runtime.getRuntime().availableProcessors()获取
        // 但是你可以通过系统属性java.util.concurrent.ForkJoinPool.common.parallelism来改变线程池大小,如下所示:
        // System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
        // 这是一个全局设置,其实线程数量等于处理器数量是很好地1默认值,所以不建议修改
        System.out.println("parallel thread count -->" + Runtime.getRuntime().availableProcessors());
        long current1 = System.currentTimeMillis();
        // 并行处理 parallelStream()
        Stream<String> s = aSet.stream();
        s.parallel().filter(str -> str != null).map(String::trim)
                        .filter(str -> str.length() > 0)
                        // ====依然返回stream的是中间操作====
                        // .distinct() 去重
                        // .limit(3) // 取前三个
                        // 包括sorted/map/skip等
                        
                        // ====返回其他类型的时终结操作====
                        // .count()
                        // .forEach()
                        .collect(Collectors.toSet());
        // 分组
        // collect(Collectors.groupingBy(String::length))
        System.out.println(
            "stream foreach --> " + (System.currentTimeMillis() - current1));

        long current2 = System.currentTimeMillis();
        Set<String> cSet = new HashSet<String>();
        for (String a : aSet)
        {
            if (a != null)
            {
                String trimStr = a.trim();
                if (trimStr.length() > 0)
                {
                    cSet.add(trimStr);
                }
            }
        }
        System.out.println(
            "set foreach --> " + (System.currentTimeMillis() - current2));

        // 流只能被消费一次 重复消费报错
        // s.filter(String::isEmpty);
        
        testSumSpeed(1000000000);
    }

}

输出:

parallel thread count -->16
stream foreach --> 101088
set foreach --> 26413
sum value --> 500000000500000000, IntStream sum done in --> 389
sum1 value --> 500000000500000000, IntStream sum1 done in --> 692
sum_ value --> 500000000500000000, foreach sum done in --> 426

参考书籍:

《Java8实战》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值