Java8 如何正确高效的使用并行流

 更新时间:2021年11月04日 16:11:38   作者:小小工匠  
这篇文章主要介绍了Java8 如何正确高效的使用并行流,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

正确使用并行流,避免共享可变状态

错用并行流而产生错误的首要原因,就是使用的算法改变了某些共享状态。下面是另一种实现对前n个自然数求和的方法,但这会改变一个共享累加器:

public static long sideEffectSum(long n) {
	Accumulator accumulator = new Accumulator();
	LongStream.rangeClosed(1, n).forEach(accumulator::add);
	return accumulator.total;
}
public class Accumulator {
	public long total = 0;
	public void add(long value) { total += value; }
}

有什么问题呢?

它在本质上就是顺序的。每次访问 total 都会出现数据竞争。如果用同步来修复,那就完全失去并行的意义了。

为了说明这一点,让我们试着把 Stream 变成并行的:

public static long sideEffectParallelSum(long n) {
	Accumulator accumulator = new Accumulator();
	LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
	return accumulator.total;
}

测试下,输出

在这里插入图片描述

在这里插入图片描述

性能无关紧要了,唯一要紧的是每次执行都会返回不同的结果,都离正确值差很远。这是由于多个线程在同时访问累加器,执行 total += value ,而这却不是一个原子操作。问题的根源在于, forEach 中调用的方法有副作用它会改变多个线程共享的对象的可变状态。

要是你想用并行 Stream 又不想引发类似的意外,就必须避免这种情况。

所以共享可变状态会影响并行流以及并行计算,要避免共享可变状态,确保并行 Stream 得到正确的结果。

高效使用并行流

是否有必要使用并行流?

  • 如果有疑问,多次测试结果。把顺序流转成并行流轻而易举,但却不一定是好事
  • 留意装箱。自动装箱和拆箱操作会大大降低性能

Java 8中有原始类型流( IntStream 、LongStream 、 DoubleStream )来避免这种操作,但?有可能都应该用这些流。

  • 有些操作本身在并行流上的性能就比顺序流差。特别是 limit 和 findFirst 等依赖于元素顺序的操作,它们在并行流上执行的代价非常大。

例如, findAny 会比 findFirst 性能好,因为它不一定要按顺序来执行。可以调用 unordered 方法来把有序流变成无序流。那么,如果你需要流中的n个元素而不是专门要前n个的话,对无序并行流调用limit 可能会比单个有序流(比如数据源是一个 List )更高效。

  • 还要考虑流的操作流水线的总计算成本。

设N是要处理的元素的总数,Q是一个元素通过流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值较高就意味着使用并行流时性能好的可能性比较大。

  • 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。并行处理少数几个元素的好处还?不上并行化造成的额外开销
  • 要考虑流背后的数据结构是否易于分解。

例如, ArrayList 的拆分效率比 LinkedList高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。另外,用 range 工厂方法创建的原始类型流也可以快速分解。

  • 流自身的特点,以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。

例如,一个 SIZED 流可以分成大小相等的两部分,这样每个部分都可以比较高效地并行处理,但筛选操作可能丢弃的元素个数却无法预测,导致流本身的大小未知。

  • 还要考虑终端操作中合并步骤的代价是大是小(例如 Collector 中的 combiner 方法)

如果这一步代价很大,那么组合每个子流产生的部分结果所付出的代价就可能会超出通过并行流得到的性能提升。

流的数据源和可分解性

在这里插入图片描述

最后, 并行流背后使用的基础架构是Java 7中引入的分支/合并框架了解它的内部原理至关重要。

java 并行计算的几点实践总结

稍微接触了 java 的并行计算,谈谈几点浅显的总结吧

并行计算不一定比串行计算快,一般在大规模问题才会显示出优势

结合 lambda 表达式的 parallelStream 可以方便调用并行计算,但可能会出现空指针错误,解决这一问题可能需要更高级的多线程知识

看网上资料,Collection 类型对并行计算支持的好,一般数组类型支持的一般。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • mybatis写xml时数字类型千万别用 !=‘‘(不为空串)进行判断的示例详解

    mybatis写xml时数字类型千万别用 !=‘‘(不为空串)进行判断的示例详解

    这篇文章主要介绍了mybatis写xml时数字类型千万别用 !=‘‘(不为空串)进行判断的示例详解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • 使用Java实现文件夹的遍历操作指南

    使用Java实现文件夹的遍历操作指南

    网上大多采用java递归的方式遍历文件夹下的文件,这里我不太喜欢递归的风格,这篇文章主要给大家介绍了关于使用Java实现文件夹的遍历操作的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-05-05
  • IntelliJ IDEA打开多个Maven的module且相互调用代码的方法

    IntelliJ IDEA打开多个Maven的module且相互调用代码的方法

    这篇文章主要介绍了IntelliJ IDEA打开多个Maven的module且相互调用代码的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-02-02
  • Java Redisson多策略注解限流

    Java Redisson多策略注解限流

    这篇文章主要介绍了Java Redisson多策略注解限流,文章使用Redisson的RRateLimiter进行限流,详细介绍,感兴趣的小伙伴可以参考下面文章内容
    2022-09-09
  • Java多线程中Thread.currentThread()和this的区别详解

    Java多线程中Thread.currentThread()和this的区别详解

    这篇文章主要介绍了Java多线程中Thread.currentThread()和this的区别详解,Thread.currentThread()方法返回的是对当前正在执行的线程对象的引用,this代表的是当前调用它所在函数所属的对象的引用,需要的朋友可以参考下
    2023-08-08
  • Java如何实现N叉树数据结构

    Java如何实现N叉树数据结构

    这篇文章主要介绍了Java如何实现N叉树数据结构问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • 支撑Java NIO与NodeJS的底层技术

    支撑Java NIO与NodeJS的底层技术

    这篇文章主要为大家详细介绍了支撑Java NIO与NodeJS的底层技术,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-09-09
  • Java面向对象基础知识之委托和lambda

    Java面向对象基础知识之委托和lambda

    这篇文章主要介绍了Java面向对象的之委托和 lambda,文中有非常详细的代码示例,对正在学习java基础的小伙伴们有很好的帮助,需要的朋友可以参考下
    2021-11-11
  • Java如何获取Date的“昨天”与“明天”示例代码

    Java如何获取Date的“昨天”与“明天”示例代码

    最近在做项目的时候用到Date和Calendar比较多,而且用到的方式也比较全,突然想到一个问题,Java如何获取Date的"昨天"与"明天",也就是前一天和后一天呢?思考后写出了方法,想着万一以后用到,就总结出来,也方便有需要的朋友们参考借鉴,下面来一起看看吧。
    2016-12-12
  • Java RSA加密解密实现方法分析【附BASE64 jar包下载】

    Java RSA加密解密实现方法分析【附BASE64 jar包下载】

    这篇文章主要介绍了Java RSA加密解密实现方法,结合实例形式分析了java基于第三方类库javabase64-1.3.1.jar实现RSA加密解密功能的具体步骤与相关操作技巧,需要的朋友可以参考下
    2017-10-10

最新评论