上篇 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实战》