探讨并行流(parallelStream)

2 篇文章 0 订阅

一、parallelStream是什么

Java8中提供了能够更方便处理集合数据的Stream类,其中parallelStream()方法能够充分利用多核CPU的优势,使用多线程加快对集合数据的处理速度。parallelStream主要用于利用处理器的多个核心。通常,任何Java代码都有一个处理流,在这里它是按顺序执行的。然而,通过使用并行流,我们可以将代码分成多个流,这些流在不同的内核上并行执行,最终的结果是各个结果的组合。然而,处理的顺序不在我们的控制之下。
因此,建议在以下情况下使用并行流:无论执行顺序如何,结果不受影响,一个元素的状态不影响另一个元素,并且数据源也不受影响。

parallelStream()方法的源码如下:

/**
 * @return a possibly parallel {@code Stream} over the elements in this
 * collection
 * @since 1.8
 */

default Stream<E> parallelStream() {
    return StreamSupport.stream(spliterator(), true);
}

从上面代码中注释的@return a possibly parallel可以看得出来,parallelStream()并不是一定返回一个并行流,有可能parallelStream()全是由主线程顺序执行的。因此使用parallelStream时要特别注意。

二、parallelStream原理分析

但是parallelStream是如何实现多线程处理的呢?其实看源码我们会发现parallelStream是使用线程池ForkJoin来调度的,并且参与并行处理的线程有主线程以及ForkJoinPool中的worker线程。

1.Fork/Join框架
parallelStream的底层是基于ForkJoinPool的,ForkJoinPool实现了ExecutorService接口,因此和线程池有着密不可分的关系。
ForkJoinPool和ExecutorService的继承关系如图所示:
在这里插入图片描述

Fork/Join框架主要采用分而治之的理念来处理问题,对于一个比较大的任务,首先将它拆分(fork)为多个小任务task1、task2等。再使用新的线程thread1去处理task1,thread2去处理task2。
如果thread1认为task1还是太大,则继续往下拆分成新的子任务task1.1与task1.2。thread2认为task2任务量不大,则立即进行处理,形成结果result2。
之后将task1.1和task1.2的处理结果合并(join)成result1,最后将result1与result2合并成最后的结果。
下面用图更清晰的进行描述:
在这里插入图片描述

1.1 work-stealing(工作窃取算法)
work-stealing(工作窃取):ForkJoinPool提供了一个更有效的利用线程的机制,当ThreadPoolExecutor还在用单个队列存放任务时,ForkJoinPool已经分配了与线程数相等的队列,当有任务加入线程池时,会被平均分配到对应的队列上,各线程进行正常工作,当有线程提前完成时,会从队列的末端“窃取”其他线程未执行完的任务,当任务量特别大时,CPU多的计算机会表现出更好的性能。
1.2 常用方法
1.ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:
RecursiveAction:用于没有返回结果的任务。
RecursiveTask:用于有返回结果的任务。
2.ForkJoinPool :ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.IntStream;
 
/**
 * @Description 提交有返回值的任务
 */
 
public class ForkJoinRecursiveTask {
 
    /**
     * 最大计算数
     */
    private static final int MAX_THRESHOLD = 100;
 
    public static void main(String[] args) {
        //创建ForkJoinPool
        ForkJoinPool pool = new ForkJoinPool();
        //异步提交RecursiveTask任务
        ForkJoinTask<Integer> forkJoinTask = pool.submit(new CalculatedRecursiveTask(0, 1000));
        try {
            //根据返回类型获取返回值
            Integer result = forkJoinTask.get();
            System.out.println("执行结果为:" + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            pool.shutdown();
        }
    }
 
    private static class CalculatedRecursiveTask extends RecursiveTask<Integer> {
        private final int start;
        private final int end;
 
        public CalculatedRecursiveTask(int start, int end) {
            this.start = start;
            this.end = end;
        }
 
        @Override
        protected Integer compute() {
            //判断计算范围,如果小于等于5,那么一个线程计算即可,否则进行分割
            if ((end - start) <= MAX_THRESHOLD) {
                //返回[start,end]的总和
                return IntStream.rangeClosed(start, end).sum();
            } else {
                //任务分割
                int middle = (end + start) / 2;
                CalculatedRecursiveTask task1 = new CalculatedRecursiveTask(start, middle);
                CalculatedRecursiveTask task2 = new CalculatedRecursiveTask(middle + 1, end);
                //执行
                task1.fork();
                task2.fork();
                //等待返回结果
                return task1.join() + task2.join();
            }
        }
    }
}

执行结果如下:
在这里插入图片描述

2.2 提交无返回值的任务

package com.example.oms.controller;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
 
/**
 * @Description 提交无返回值的任务
 */
 
public class ForkJoinRecursiveAction {
 
    /**
     * 最大计算数
     */
    private static final int MAX_THRESHOLD = 100;
    private static final AtomicInteger SUM = new AtomicInteger(0);
 
    public static void main(String[] args) throws InterruptedException {
        //创建ForkJoinPool
        ForkJoinPool pool = new ForkJoinPool();
        //异步提交RecursiveAction任务
        pool.submit(new CalculatedRecursiveTask(0, 1000));
        //等待3秒后输出结果,因为计算需要时间
        pool.awaitTermination(1, TimeUnit.SECONDS);
        System.out.println("结果为:" + SUM);
        pool.shutdown();
    }
 
    private static class CalculatedRecursiveTask extends RecursiveAction {
        private final int start;
        private final int end;
 
        public CalculatedRecursiveTask(int start, int end) {
            this.start = start;
            this.end = end;
        }
 
        @Override
        protected void compute() {
            //判断计算范围,如果小于等于5,那么一个线程计算即可,否则进行分割
            if ((end - start) <= MAX_THRESHOLD) {
                //因为没有返回值,所有这里如果要获取结果,需要存入公共的变量中
                SUM.addAndGet(IntStream.rangeClosed(start, end).sum());
            } else {
                //任务分割
                int middle = (end + start) / 2;
                CalculatedRecursiveTask task1 = new CalculatedRecursiveTask(start, middle);
                CalculatedRecursiveTask task2 = new CalculatedRecursiveTask(middle + 1, end);
                //执行
                task1.fork();
                task2.fork();
            }
        }
    }
}

虽然ForkJoin实际的代码非常复杂,但是通过这个例子应该了解到ForkJoinPool底层的分治算法和工作窃取原理。ForkJoin不仅在Java8之后的Stream中广泛使用。golang等其他语言的协程机制,也是采用类似的原理来实现的。

二、使用方法

1.为什么使用并行流

并行流的引入是为了提高程序的性能,但是选择并行流并不总是最好的选择。在某些情况下,我们需要以特定的顺序执行代码,在这些情况下,我们最好使用顺序流以牺牲性能为代价来执行任务。这两种流之间的性能差异仅在大型程序或复杂项目中才值得关注。对于小规模的项目,它甚至可能不明显。基本上,当顺序流表现不佳时,您应该考虑使用并行流。
1.Stream和parallelStream选择

在从stream和parallelStream方法中进行选择时,我们可以考虑以下几个问题:

1.是否需要并行?
2.任务之间是否是独立的?是否会引起任何竞态条件?
3.结果是否取决于任务的调用顺序?

对于问题1,在回答这个问题之前,需要明确要解决的问题是什么,数据量有多大,计算的特点是什么?并不是所有的问题都适合使用并发程序来求解,比如当数据量不大时,顺序执行往往比并行执行更快。毕竟,准备线程池和其它相关资源也是需要时间的。但是,当任务涉及到I/O操作并且任务之间不互相依赖时,那么并行化就是一个不错的选择。通常而言,将这类程序并行化之后,执行速度会提升好几个等级。

对于问题2,如果任务之间是独立的,并且代码中不涉及到对同一个对象的某个状态或者某个变量的更新操作,那么就表明代码是可以被并行化的。

对于问题3,由于在并行环境中任务的执行顺序是不确定的,因此对于依赖于顺序的任务而言,并行化也许不能给出正确的结果。

三、注意事项

因为是并行流,所以所涉及到的数据结构需要使用线程安全的。例如

listByPage.parallelStream().forEach(str-> {
//使用线程安全的数据结构
//ConcurrentHashMap
//CopyOnWriteArrayList
//等等进行操作
});

线程关联的ThreadLocal将会失效。
由于开头提到的主线程有可能参与到parallelStream中的任务处理的过程中。因此如果我们处理的任务方法中包含对ThreadLocal的处理,可能除主线程之外的所有线程都获取不到自己的线程局部变量,加之ForkJoinPool中的线程是反复使用的,线程关联的ThreadLocal会发生共用的情况。

所以我的建议是,parallelStream中就不要使用ThreadLocal了,要么在任务处理方法中,第一行先进行ThreadLocal.set(),之后再由ThreadLocal.get()获取到自己的线程局部变量
使用并行流时,不要使用collectors.groupingBy、collectors.toMap
使用并行流时,不要使用collectors.groupingBy、collectors.toMap,替代为collectors.groupingByConcurrent、collectors.toConcurrentMap,或直接使用串行流。

原因,并行流执行时,通过操作Key来合并多个map的操作比较昂贵。详细大家可以查看官网介绍。

不安全例子

public void doThreadUnSafe() {
        List<Integer> listFor = new ArrayList<>();
        List<Integer> listParallel = new ArrayList<>();

        IntStream.range(0, 1000).forEach(listFor::add);
        IntStream.range(0, 1000).parallel().forEach(listParallel::add);

        System.out.println("listFor size :" + listFor.size());
        System.out.println("listParallel size :" + listParallel.size());
    }

输出结果:
listFor size :1000
listParallel size :949

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zcy_code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值