Java8 并行流(parallelStream)原理分析及注意事项

2023-11-10

文章目录


前言

众所周知,Java 使用Stream流做多线程处理是非常方便的。随着并行编程越来越流行,Java从1.7就开始提供了Fork/Join 支持并行处理,并且在1.8版本进一步加强了相关功能。并行处理就是将任务拆分子任务,分发给多个处理器同时处理之后进行合并。下面将会对并行流(parallelStream)原理分析及注意事项进行详细介绍。


一、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原理分析

我们都知道在java 使用strem流做多线程处理是非常方便的。

list.parallelStream().forEach(s -> {
            // 后续业务处理
        });

但是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合并成最后的结果。

下面用图更清晰的进行描述:
 

Fork/Join流程图

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来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

2. 实例演示

2.1 提交有返回值的任务

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 提交无返回值的任务

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. 为什么使用并行流

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

2. Stream和parallelStream选择

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

1.是否需要并行?

2.任务之间是否是独立的?是否会引起任何竞态条件?

3.结果是否取决于任务的调用顺序?

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

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

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

3. 正确使用并行流

并行流并不总是比顺序流快。所以正确的姿势使用并行流是尤为重要的,不然适得其反。

决定某个特定情况下是否有必要使用并行流。可以参考一下几点建议

  • 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工厂方法创建的原始类型流也可以快速分解。可以参考一下表格:

数据源 性能
ArrayList 极佳
LinkedList
IntStrean.range 极佳
Strean.iterate
HashSet
TreeSet
  • 7、流自身的特点以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。例如,一个SIZED流可以分成大小相等的两部分,这样每个部分都可以比较高效地并行处理,但筛选操作可能丢弃的元素个数无法预测,从而导致流本身的大小未知。

  • 8、还要考虑终端操作中合并步骤的代价是大是小(例如Collector中的combiner方法)。如果这一步代价很大,那么组合每个子流产生的部分结果所付出的代价就可能会超出通过并行流得到的性能提升。

三、注意事项

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

    listByPage.parallelStream().forEach(str-> {
               //使用线程安全的数据结构
               //ConcurrentHashMap
               //CopyOnWriteArrayList
               //等等进行操作
            });
  2. 线程关联的ThreadLocal将会失效。

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

    所以我的建议是,parallelStream中就不要使用ThreadLocal了,要么在任务处理方法中,第一行先进行ThreadLocal.set(),之后再由ThreadLocal.get()获取到自己的线程局部变量

  3. 使用并行流时,不要使用collectors.groupingBy、collectors.toMap

    使用并行流时,不要使用collectors.groupingBy、collectors.toMap,替代为collectors.groupingByConcurrent、collectors.toConcurrentMap,或直接使用串行流。

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

    https://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html#concurrent_reduction

  4. 使用parallelStream也不一定会提升性能

    在CPU资源紧张的时候,使用并行流可能会带来频繁的线程上下文切换,导致并行流执行的效率还没有串行执行的效率高。


总结

本文对Java8的并行流(parallelStream)原理分析及注意事项进行了详细的介绍,主要对其中的Fork Join、线程池、使用方法进行了深刻的分析。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java8 并行流(parallelStream)原理分析及注意事项 的相关文章

  • 唯一索引或主键违规:“PRIMARY KEY ON PUBLIC.xxx”; SQL语句

    每当我的应用程序启动时 我都会收到以下错误消息 Caused by org h2 jdbc JdbcSQLException Unique index or primary key violation PRIMARY KEY ON PUBL
  • 菜单未显示在应用程序中

    由于某种原因 我的操作菜单在我的 Android Studio 应用程序中消失了 我正在按照教程学习如何创建 Android 应用程序 但最终遇到了这个问题 我正在使用 atm 的教程 http www raywenderlich com
  • 线程自动利用多个CPU核心?

    假设我的应用程序运行 2 个线程 例如渲染线程和游戏更新线程 如果它在具有多核 CPU 当今典型 的移动设备上运行 我是否可以期望线程在可能的情况下自动分配给不同的核心 我知道底层操作系统内核 Android linux内核 决定调度 我的
  • java.io.IOException: %1 不是有效的 Win32 应用程序

    我正在尝试对 XML 文档进行数字签名 为此我有两个选择 有一个由爱沙尼亚认证中心为程序员创建的库 还有一个由银行制作的运行 Java 代码的脚本 如果使用官方 认证中心 库 那么一切都会像魅力一样进行一些调整 但是当涉及到银行脚本时 它会
  • java中删除字符串中的特殊字符?

    如何删除字符串中除 之外的特殊字符 现在我用 replaceAll w s 它删除了所有特殊字符 但我想保留 谁能告诉我我该怎么办 Use replaceAll w s 我所做的是将下划线和连字符添加到正则表达式中 我添加了一个 连字符之前
  • HDFS:使用 Java / Scala API 移动多个文件

    我需要使用 Java Scala 程序移动 HDFS 中对应于给定正则表达式的多个文件 例如 我必须移动所有名称为 xml从文件夹a到文件夹b 使用 shell 命令我可以使用以下命令 bin hdfs dfs mv a xml b 我可以
  • 一种使用 Java Robot API 和 Selenium WebDriver by Java 进行文件上传的解决方案

    我看到很多人在使用 Selenium WebDriver 的测试环境中上传文件时遇到问题 我使用 selenium WebDriver 和 java 也遇到了同样的问题 我终于找到了解决方案 所以我将其发布在这里希望对其他人有所帮助 当我需
  • jdbc4.MySQLSyntaxErrorException:数据库中不存在表

    我正在使用 SpringBoot 开发一个网络应用程序 这是我的application properties文件来指定访问数据库的凭据 spring datasource driverClassName com mysql jdbc Dri
  • hibernate总是自己删除表中的所有数据

    您好 我正在开发一个 spring mvc 应用程序 它使用 hibernate 连接到存储文件的 mysql 数据库 我有两个方法 一个方法添加我选择的特定文件路径中的所有文件 另一种方法调用查询以返回从 mysql 存储的文件列表 问题
  • OnClick 事件中的 finish() 如何工作?

    我有一个Activity一键退出Activity 通过layout xml我必须设置OnClick事件至cmd exit调用 this finish 效果很好 public void cmd exit View editLayout thi
  • 从 android 简单上传到 S3

    我在网上搜索了从 android 上传简单文件到 s3 的方法 但找不到任何有效的方法 我认为这是因为缺乏具体步骤 1 https mobile awsblog com post Tx1V588RKX5XPQB TransferManage
  • 制作java包

    我的 Java 类组织变得有点混乱 所以我要回顾一下我在 Java 学习中跳过的东西 类路径 我无法安静地将心爱的类编译到我为它们创建的包中 这是我的文件夹层次结构 com david Greet java greeter SayHello
  • 尝试使用 Ruby Java Bridge (RJB) gem 时出现错误“无法创建 Java VM”

    我正在尝试实现 Ruby Java Bridge RJB gem 来与 JVM 通信 以便我可以运行 Open NLP gem 我在 Windows 8 上安装并运行了 Java 所有迹象 至少我所知道的 都表明 Java 已安装并可运行
  • 如何在 Maven 中显示消息

    如何在 Maven 中显示消息 在ant中 我们确实有 echo 来显示消息 但是在maven中 我该怎么做呢 您可以使用 antrun 插件
  • Windows 上的 Nifi 命令

    在我当前的项目中 我一直在Windows操作系统上使用apache nifi 我已经提取了nifi 0 7 0 bin zip文件输入C 现在 当我跑步时 bin run nifi bat as 管理员我在命令行上看到以下消息 但无法运行
  • 运行 Jar 文件时出现问题

    我已将 java 项目编译成 Jar 文件 但运行它时遇到问题 当我跑步时 java jar myJar jar 我收到以下错误 Could not find the main class myClass 类文件不在 jar 的根目录中 因
  • Android JNI C 简单追加函数

    我想制作一个简单的函数 返回两个字符串的值 基本上 java public native String getAppendedString String name c jstring Java com example hellojni He
  • 将2-3-4树转换为红黑树

    我正在尝试将 2 3 4 树转换为 java 中的红黑树 但我无法弄清楚它 我将这两个基本类编写如下 以使问题简单明了 但不知道从这里到哪里去 public class TwoThreeFour
  • 如何修复“sessionFactory”或“hibernateTemplate”是必需的问题

    我正在使用 Spring Boot JPA WEB 和 MYSQL 创建我的 Web 应用程序 它总是说 sessionFactory or hibernateTemplate是必需的 我该如何修复它 我已经尝试过的东西 删除了本地 Mav
  • java迭代器内部是如何工作的? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我有一个员工列表 List

随机推荐

  • 在Android Studio中下载Android SDK的两种方式(第二种好用)

    转自 https www cnblogs com mxj961116 p 10423479 html Android studio下载地址 http www android studio org 方式一 设置HTTP Proxy1 打开Se
  • synchronized 关键字和 volatile 关键字的区别

    synchronized 关键字和 volatile 关键字是两个互补的存在 而不是对立的存在 两者主要有一下区别 1 volatile 关键字是线程同步的轻量级实现 所以 volatile性能肯定比synchronized关键字要好 2
  • Spring3.0带来的新特性

    一 首先 模块的组织更加的细致 从那么多的jar分包就看的出来 Spring的构建系统以及依赖管理使用的是Apache Ivy 从源码包看出 也使用了Maven Maven确实是个好东西 好处不再多言 以后希望能进一步用好它 二 新特性如下
  • Response.Redirect和Server.Transfer的比较

    Response Redirect响应重定向方法 Response将url返回给客户端浏览器 客户端浏览器向服务器发送重定向请求 服务器接到请求后将响应头部返回给客户端 客户端收到响应头部后发送一个新的重定向请求给服务器 Server Tr
  • 端口号被占用,如何终止?

    目录 1 使用快捷键Windows R 输入cmd进入dos命令窗口 2 输入netstat nao 查看本地所有的端口号信息 3 然后在输入 taskkill pid 数字代号 f 4 回车即可 将数字代号所对应的端口号给结束 5 数字代
  • 五、webpack的基本使用,防止重复,入口文件,懒加载,预获取/预加载(Mhua)

    入口文件 webpack 打包文件时 可以拆分多个入口文件 首先安装 lodash 插件 npm install lodash 在入口文件 index js 配置如下内容 import imgSrc from assets 2 png im
  • 【Ensemble Learning】第 4 章:混合组合

    在前面的章节中 我们讨论了如何混合训练数据 以及如何混合机器学习模型来创建更强大的模型 利用集成学习的力量 让我们继续这个学习过程 在本章中 我们介绍并解释了两种强大的集成学习技术 它们利用机器学习模型的混合组合来构建更强大的模型 我们一次
  • 在windows 上安装 openSSH

    一 基础环境 操作系统 Microsoft Windows Server 2019 datacenter 64位 openSSH 版本 OpenSSH for Windows 8 6p1 LibreSSL 3 3 3 二 操作步骤 1 下载
  • python语言turtle库画图代码示例_5分钟轻松搞定,Python开发之turtle库的基本操作...

    文源网络 仅供学习之用 如有侵权请联系删除 基础总结 turtle库是python标准库之一 入门级绘图库 import turtle之后即可使用 turtle绘图原理 有一只海龟 其实在窗体正中心 在画布上游 走过的轨迹形成了绘制的图形
  • R及RStudio下载安装教程(超详细)

    R 语言是为数学研究工作者设计的一种数学编程语言 主要用于统计分析 绘图 数据挖掘 如果你是一个计算机程序的初学者并且急切地想了解计算机的通用编程 R 语言不是一个很理想的选择 可以选择 Python C 或 Java R 语言与 C 语言
  • 模板的类型萃取

    初次接触类型萃取是在运用模板实现seqlist的时候 拷贝构造和赋值运算符重载时 单纯的使用memcopy 函数进行拷贝 只是单纯的进行了浅拷贝 对于基本的数据类型是不会有任何错误的 但是如果是string类型时 单纯的值拷贝显然是不行的
  • AttributeError: ‘xxx‘ object has no attribute ‘__bases__‘ 问题解决

    问题描述 这是我的代码 本体为一个pytorch模型 希望通过查看父类继承确定是否为网络模型 class Net nn Module def init self super Net self init self conv1 nn Conv2
  • STM32F103XX扫描I2C从机地址main.c(改进版)

    include stm32f10x h include Delay h include OLED h include MyI2C h uint8 t i 0x00 Addr Count 0 int main void OLED Init M
  • 人人组队与人机组队的风险

    无论人与人之间还是人与机之间 只要有协同就会有风险 其原因主要是 协同可能导致合作伙伴之间的利益冲突 协同合作还可能引发信息共享的风险 协同可能面临合作对象的信任问题 协同合作还可能受到外部环境的影响等等 人人组队的风险主要涉及以下几个方面
  • 开发svn hook阻止svn仓库特定分支的commit log中不包含关键字的提交

    使用批处理脚本实现开发svn hook pre commit 阻止svn仓库特定分支的commit log中不包含关键字的提交 批处理脚本 shell脚本实现 实现逻辑 1 使用svnlook 的dirs changed子命令去获取当前仓库
  • Linux system函数返回值

    http blog cheyo net p 42 例 1 status system test sh 1 先统一两个说法 1 system返回值 指调用system函数后的返回值 比如上例中status为system返回值 2 shell返
  • 尼康D90使用心得

    文章目录 规格参数 快速指南 相机机身 模式拨盘 控制面板 取景器 拍摄信息展示 核心功能 指令拨盘 拍摄模式 自动模式 场景模式 快门速度和光圈 固件 软件 驱动升级 更多细节参考 规格参数 型号 尼康D90 发布日期 2008年08月
  • 全国计算机等级考试题库二级C操作题100套(第90套)

    第90套 函数fun的功能是 统计所有小于等于n n gt 2 的素数的个数 素数的个数作为函数值返回 请在程序的下划线处填入正确的内容并把下划线删除 使程序得出正确的结果 注意 源程序存放在考生文件夹下的BLANK1 C中 不得增行或删行
  • ARM架构内核启动分析-head.S(1.1、vmlinux.lds 链接脚本分析)

    ARM架构内核启动分析 一 start kernel之前 首先需要明确的是 内核镜像在被解压之后执行 是执行哪段代码 这是个重要的问题 平时在编译生成应用程序或内核模块时 我们无需考虑链接的具体细节 如代码和数据放在哪里 代码执行入口在哪等
  • Java8 并行流(parallelStream)原理分析及注意事项

    文章目录 前言 一 parallelStream是什么 二 parallelStream原理分析 1 Fork Join框架 1 1 work stealing 工作窃取算法 1 2 常用方法 2 实例演示 2 1 提交有返回值的任务 2