Java8新特性ParallelStream并行流详细说明和注意事项

本文深入探讨Java8中并行流的效率、准确性及其实现机制,通过实验对比并行流与其他遍历方式在不同数据量下的表现,揭示并行流在大数据处理中的优势及其适用场景。

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

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以前,遍历一个长度非常大的集合往往非常麻烦,如需要使用多个线程配合synchronizedLockAtomic原子引用等进行遍历,且不说多线程之间的调度,多线程同步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");
});

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

普通for增强for串行流并行流
40.92340.98139.39119.647

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

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

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

普通for增强for串行流并行流
2.062.412.831.68

对结果进行分析,现在开发中比较少见的普通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提供的并行流中,在实现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时,每次请求消耗的时间

普通for增强for串行流并行流
4.2754.2974.3333.64

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

普通for增强for串行流并行流
200.476194.503192.569150.101

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

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

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

文章如果哪个地方有纰漏的话,欢迎跟我留言或者私信反馈,十分感谢您的支持!

如果这篇文章对您有所帮助的话,希望您可以点个赞或者收藏一下文章支持一下哦。

<think>嗯,用户想学习Java 8Stream流,该怎么教他们呢?首先得了解他们的背景。可能他们已经有一些Java基础,但没接触过Stream。需要从基础开始,逐步深入。 首先,得介绍Stream是什么。用户可能听说过Stream处理集合更方便,但具体概念还不清楚。要解释Stream集合的区别,比如元素序列、源数据、函数式操作这些点。可能还要提到中间操作终端操作的区别,比如filter是中间操作,collect是终端操作,这样他们不会混淆。 然后,需要举一些简单的例子,比如创建Stream的方式。从集合转换是最常见的,可以用stream()方法。数组的话用Arrays.stream,或者直接用Stream.of。例子要简单明了,比如用List转Stream,这样用户能马上上手。 接下来是常用操作。filter、map、sorted这些中间操作,还有collect、forEach这些终端操作。每个操作都需要例子说明。比如用filter过滤字符串长度,用map转换成大写,sorted排序。例子代码要简洁,展示出链式调用的优势,对比传统循环,突出Stream的简洁性。 然后,可能用户会想知道更多高级操作,比如flatMap处理嵌套集合,reduce进行归约,还有统计操作如sum、average。这时候需要例子展示如何扁平化流,比如把多个列表合并,或者计算总。这些例子能帮助用户理解更复杂的场景。 并行流也是一个重点,毕竟Stream的优势之一就是并行处理。需要解释parallelStream的使用,但也要提醒线程安全的问题,比如在reduce操作中累加器要符合规范,避免并发问题。例子用并行流计算总,对比串行流,说明性能提升的可能性。 接下来,学习资源推荐。官方文档是必须的,但可能有些用户觉得枯燥,所以推荐一些书籍,比如《Java 8实战》,还有在线教程如Baeldung,这些资源比较友好。另外,实践项目很重要,可以建议他们用Stream重构旧代码,或者处理数据,比如统计日志信息,这样能巩固所学。 最后,注意事项部分要强调Stream的一次性消费,避免重复使用;区分中间终端操作,防止遗漏终端操作导致不执行;性能方面,小数据可能传统循环更快,大数据用并行流。这些注意事项能帮助用户避免常见错误。 整个思考过程中,要确保结构清晰,从概念到操作,再到高级特性资源,逐步引导用户。例子要具体,避免太抽象的解释,让用户能动手实践。同时,提醒他们注意事项,避免踩坑。可能用户需要对比传统方式Stream的区别,所以在例子中适当比较,突出Stream的优点。还要考虑用户可能遇到的问题,比如为什么终端操作没执行,或者并行流的线程安全,提前给出提示。</think>要系统学习Java 8Stream流,可以按以下步骤逐步掌握: --- ### 一、理解核心概念 1. **什么是Stream**: - 不是数据结构,而是对数据源(集合、数组等)进行函数式操作的流水线 - 特点:不存储数据、不修改源数据、延迟执行 2. **与集合的区别**: $$集合 = 存储 + 操作数据 \quad vs \quad Stream = 声明式操作$$ --- ### 二、掌握基础操作 #### 1. 创建Stream ```java // 从集合创建 List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream = list.stream(); // 从数组创建 String[] array = {"a", "b"}; Stream<String> stream = Arrays.stream(array); // 直接创建 Stream<String> stream = Stream.of("a", "b"); ``` #### 2. 常用中间操作 ```java List<String> result = list.stream() .filter(s -> s.length() > 3) // 过滤 .map(String::toUpperCase) // 转换 .sorted() // 排序 .distinct() // 去重 .skip(1) // 跳过元素 .limit(5) // 限制数量 .collect(Collectors.toList()); // 终端操作 ``` --- ### 三、掌握进阶特性 #### 1. 并行流 ```java long count = list.parallelStream() // 自动并行处理 .filter(s -> s.contains("a")) .count(); ``` #### 2. 数值流(避免装箱) ```java IntStream.rangeClosed(1, 100) // 生成1-100的int流 .sum(); ``` #### 3. 高级收集器 ```java Map<Integer, List<String>> groupByLength = list.stream() .collect(Collectors.groupingBy(String::length)); String joined = list.stream() .collect(Collectors.joining(", ")); ``` --- ### 四、典型应用场景 1. **数据过滤**: ```java List<User> adults = users.stream() .filter(u -> u.getAge() >= 18) .collect(Collectors.toList()); ``` 2. **数据转换**: ```java List<String> emails = users.stream() .map(User::getEmail) .collect(Collectors.toList()); ``` 3. **统计计算**: ```java Double average = users.stream() .collect(Collectors.averagingInt(User::getAge)); ``` --- ### 五、学习资源推荐 1. **官方文档**: - [Java 8 Stream API](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html) 2. **实战书籍**: - 《Java 8实战》(Java 8 in Action) 3. **在线教程**: - Baeldung的[Java Stream指南](https://www.baeldung.com/java-8-streams) - 菜鸟教程的[Java 8新特性](https://www.runoob.com/java/java8-new-features.html) --- ### 六、练习建议 1. **重构现有代码**:将传统for循环改写成Stream操作 2. **解决实际问题**: - 统计日志文件中不同错误类型的出现次数 - 处理嵌套集合(如`List<List<Integer>>`) 3. **参与编程挑战**:在LeetCode等平台用Stream解题 --- ### 七、注意事项 1. **流只能消费一次**:终端操作后流即关闭 2. **区分中间/终端操作**:缺少终端操作时中间操作不会执行 3. **避免副作用**:不要在lambda表达式中修改外部变量 4. **性能权衡**:小数据集可能传统循环更快,大数据集用并行流更高效 通过理论+实践结合,约20小时系统学习即可掌握核心用法。建议从简单案例入手,逐步尝试复杂场景的流式处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值