关于Java8 parallelStream并发安全的深入讲解

背景

Java8的stream接口极大地减少了for循环写法的复杂性,stream提供了map/reduce/collect等一系列聚合接口,还支持并发操作:parallelStream。

在爬虫开发过程中,经常会遇到遍历一个很大的集合做重复的操作,这时候如果使用串行执行会相当耗时,因此一般会采用多线程来提速。Java8的paralleStream用fork/join框架提供了并发执行能力。但是如果使用不当,很容易陷入误区。

Java8的paralleStream是线程安全的吗

一个简单的例子,在下面的代码中采用stream的forEach接口对1-10000进行遍历,分别插入到3个ArrayList中。其中对第一个list的插入采用串行遍历,第二个使用paralleStream,第三个使用paralleStream的同时用ReentryLock对插入列表操作进行同步:

private static List<Integer> list1 = new ArrayList<>();
private static List<Integer> list2 = new ArrayList<>();
private static List<Integer> list3 = new ArrayList<>();
private static Lock lock = new ReentrantLock();

public static void main(String[] args) {
 IntStream.range(0, 10000).forEach(list1::add);

 IntStream.range(0, 10000).parallel().forEach(list2::add);

 IntStream.range(0, 10000).forEach(i -> {
 lock.lock();
 try {
  list3.add(i);
 }finally {
  lock.unlock();
 }
 });

 System.out.println("串行执行的大小:" + list1.size());
 System.out.println("并行执行的大小:" + list2.size());
 System.out.println("加锁并行执行的大小:" + list3.size());
}

执行结果:

串行执行的大小:10000
并行执行的大小:9595
加锁并行执行的大小:10000

并且每次的结果中并行执行的大小不一致,而串行和加锁后的结果一直都是正确结果。显而易见,stream.parallel.forEach()中执行的操作并非线程安全。

那么既然paralleStream不是线程安全的,是不是在其中的进行的非原子操作都要加锁呢?我在stackOverflow上找到了答案:

  • https://codereview.stackexchange.com/questions/60401/using-java-8-parallel-streams
  • https://stackoverflow.com/questions/22350288/parallel-streams-collectors-and-thread-safety

在上面两个问题的解答中,证实paralleStream的forEach接口确实不能保证同步,同时也提出了解决方案:使用collect和reduce接口。

  • http://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html

在Javadoc中也对stream的并发操作进行了相关介绍:

The Collections Framework provides synchronization wrappers, which add automatic synchronization to an arbitrary collection, making it thread-safe.

Collections框架提供了同步的包装,使得其中的操作线程安全。

所以下一步,来看看collect接口如何使用。

stream的collect接口

闲话不多说直接上源码吧,Stream.java中的collect方法句柄:

<R, A> R collect(Collector<? super T, A, R> collector);

在该实现方法中,参数是一个Collector对象,可以使用Collectors类的静态方法构造Collector对象,比如Collectors.toList(),toSet(),toMap(),etc,这块很容易查到API故不细说了。

除此之外,我们如果要在collect接口中做更多的事,就需要自定义实现Collector接口,需要实现以下方法:

Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();

要轻松理解这三个参数,要先知道fork/join是怎么运转的,一图以蔽之:

上图来自:http://www.infoq.com/cn/articles/fork-join-introduction

简单地说就是大任务拆分成小任务,分别用不同线程去完成,然后把结果合并后返回。所以第一步是拆分,第二步是分开运算,第三步是合并。这三个步骤分别对应的就是Collector的supplier,accumulator和combiner。talk is cheap show me the code,下面用一个例子来说明:

输入是一个10个整型数字的ArrayList,通过计算转换成double类型的Set,首先定义一个计算组件:

Compute.java:

public class Compute {
public Double compute(int num) {
 return (double) (2 * num);
}
}

接下来在Main.java中定义输入的类型为ArrayList的nums和类型为Set的输出结果result:

private List<Integer> nums = new ArrayList<>();
private Set<Double> result = new HashSet<>();

定义转换list的run方法,实现Collector接口,调用内部类Container中的方法,其中characteristics()方法返回空set即可:

public void run() {
 // 填充原始数据,nums中填充0-9 10个数
 IntStream.range(0, 10).forEach(nums::add);
 //实现Collector接口
 result = nums.stream().parallel().collect(new Collector<Integer, Container, Set<Double>>() {

 @Override
 public Supplier<Container> supplier() {
  return Container::new;
 }

 @Override
 public BiConsumer<Container, Integer> accumulator() {
  return Container::accumulate;
 }

 @Override
 public BinaryOperator<Container> combiner() {
  return Container::combine;
 }

 @Override
 public Function<Container, Set<Double>> finisher() {
  return Container::getResult;
 }

 @Override
 public Set<Characteristics> characteristics() {
  // 固定写法
  return Collections.emptySet();
 }
 });
}

构造内部类Container,该类的作用是一个存放输入的容器,定义了三个方法:

  • accumulate方法对输入数据进行处理并存入本地的结果
  • combine方法将其他容器的结果合并到本地的结果中
  • getResult方法返回本地的结果

Container.java:

class Container {
 // 定义本地的result
 public Set<Double> set;

 public Container() {
 this.set = new HashSet<>();
 }

 public Container accumulate(int num) {
 this.set.add(compute.compute(num));
 return this;
 }

 public Container combine(Container container) {
 this.set.addAll(container.set);
 return this;
 }

 public Set<Double> getResult() {
 return this.set;
 }
}

在Main.java中编写测试方法:

public static void main(String[] args) {
 Main main = new Main();
 main.run();
 System.out.println("原始数据:");
 main.nums.forEach(i -> System.out.print(i + " "));
 System.out.println("\n\ncollect方法加工后的数据:");
 main.result.forEach(i -> System.out.print(i + " "));
}

输出:

原始数据:
0 1 2 3 4 5 6 7 8 9

collect方法加工后的数据:
0.0 2.0 4.0 8.0 16.0 18.0 10.0 6.0 12.0 14.0

我们将10个整型数值的list转成了10个double类型的set,至此验证成功~

本程序参考 http://blog.csdn.net/io_field/article/details/54971555。

一言蔽之

总结就是paralleStream里直接去修改变量是非线程安全的,但是采用collect和reduce操作就是满足线程安全的了。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Java8中利用stream对map集合进行过滤的方法

    前言 Stream 是用函数式编程方式在集合类上进行复杂操作的工具,其集成了Java 8中的众多新特性之一的聚合操作,开发者可以更容易地使用Lambda表达式,并且更方便地实现对集合的查找.遍历.过滤以及常见计算等. 最近公司在大张旗鼓的进行代码审核,从中也发现自己写代码的不好习惯.一次无意的点到了公司封装的对map集合过滤的方法,发现了stream.于是研究了一下.并对原有的代码再次结合Optional进行重构下 原有方法说明 主要处理过滤条件Map对象,过滤掉了null和空字符串 等操作 这

  • Java8处理集合的优雅姿势之Stream

    前言 在Java中,集合和数组是我们经常会用到的数据结构,需要经常对他们做增.删.改.查.聚合.统计.过滤等操作.相比之下,关系型数据库中也同样有这些操作,但是在Java 8之前,集合和数组的处理并不是很便捷. 不过,这一问题在Java 8中得到了改善,Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据.本文就来介绍下如何使用Stream.特别说明一下,关于Stream的性能及原理不是本文的重点,如果大家感兴趣后面会出文章单独介绍. 1.Stream介绍

  • java8中parallelStream性能测试及结果分析

    测试1 @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 20, time = 3, timeUnit = TimeUnit.SECONDS) @Fork(1) @State(Scope.Benchmark) public cla

  • java8中Stream的使用示例教程

    前言 Java8中提供了Stream对集合操作作出了极大的简化,学习了Stream之后,我们以后不用使用for循环就能对集合作出很好的操作. 本文将给大家详细介绍关于java8 Stream使用的相关内容,下面话不多说了,来一起看看详细的介绍吧 1. 原理 Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator. 原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作: 高级版本的 Stream,用户只要给出需

  • 详解Java8 Collect收集Stream的方法

    Collection, Collections, collect, Collector, Collectos Collection是Java集合的祖先接口. Collections是java.util包下的一个工具类,内涵各种处理集合的静态方法. java.util.stream.Stream#collect(java.util.stream.Collector<? super T,A,R>)是Stream的一个函数,负责收集流. java.util.stream.Collector 是一个收

  • Java8如何构建一个Stream示例详解

    Stream初体验 Stream是Java8中操作集合的一个重要特性,我们先来看看Java里面是怎么定义Stream的: "A sequence of elements supporting sequential and parallel aggregate operations." 我们来解读一下上面的那句话: 1.Stream是元素的集合,这点让Stream看起来用些类似Iterator: 2.可以支持顺序和并行的对原Stream进行汇聚的操作. Stream的创建方式有很多种,除

  • java8使用Stream API方法总结

    Stream是java8中处理集合的关键抽象概念,它可以指定您希望对集合进行的操作,可以执行非常复杂的查找.过滤和映射数据等操作.使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询. Stream 的三个操作步骤 1.创建Stream. 得到Stream流的第一种方式: 可以通过Collection系列集合提供提供的Stream()或parallelStream @Test public void test1() { //可以通过Collection系列集合提供提供的

  • 关于Java8 parallelStream并发安全的深入讲解

    背景 Java8的stream接口极大地减少了for循环写法的复杂性,stream提供了map/reduce/collect等一系列聚合接口,还支持并发操作:parallelStream. 在爬虫开发过程中,经常会遇到遍历一个很大的集合做重复的操作,这时候如果使用串行执行会相当耗时,因此一般会采用多线程来提速.Java8的paralleStream用fork/join框架提供了并发执行能力.但是如果使用不当,很容易陷入误区. Java8的paralleStream是线程安全的吗 一个简单的例子,

  • Redis并发访问问题详细讲解

    目录 前言 什么场景需要控制并发访问 并发访问的控制方法 1.加入锁机制 2.操作原子化 小结 前言 我们在使用Redis的过程中,难免会遇到并发访问及数据更新的问题.但很多场景对数据的并发修改是很敏感的,比如库存数据如果没有做好并发读取和更新的版本控制,就会导致严重的业务问题.今天就来说说应该如何做好并发访问及数据更新问题. 什么场景需要控制并发访问 需要控制并发访问,说明这些并发的访问可能会对其他的访问造成影响.比如上面提到的库存问题,若同一时期有多个客户端访问商品A的库存数据,并且可能要更

  • Java8 Comparator: 列表排序的深入讲解

    在本文中,我们将看到几个关于如何在Java 8中对List进行排序的示例. 1.按字母顺序排序字符串列表 List<String> cities = Arrays.asList( "Milan", "london", "San Francisco", "Tokyo", "New Delhi" ); System.out.println(cities); //[Milan, london, San

  • 深入浅出Java8中parallelStream的使用

    about Stream 什么是流? Stream是java8中新增加的一个特性,被java猿统称为流. Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator.原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作:高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 "过滤掉长度大于 10 的字符串"."获取每个字符串的首字母"等,Stream 会隐式地在

  • 通过实例解析java8中的parallelStream

    这篇文章主要介绍了通过实例解析java8中的parallelStream,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 about Stream 什么是流? Stream是java8中新增加的一个特性,被java猿统称为流. Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator.原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作:高级版本的 Stream

  • 基于Java8并行流(parallelStream)的注意点

    Java8并行流(parallelStream)注意点 在最初使用并行流的时候,查询列表会偶尔性报空指针异常,这令我非常纳闷 代码是这样的: List<OrderListVO> orderListVOS = new LinkedList<OrderListVO>(); baseOrderBillList.parallelStream().forEach(baseOrderBill -> { OrderListVO orderListVO = new OrderListVO()

  • 基于CopyOnWriteArrayList并发容器(实例讲解)

    CopyOnWriteArrayList并发容器 Copy-On-Write简称COW,是一种用于程序设计中的优化策略.其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略.从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet.CopyOnWrite容器非常有用,可以在非常多的

  • java并发编程专题(八)----(JUC)实例讲解CountDownLatch

    CountDownLatch 是一个非常实用的多线程控制工具类." Count Down " 在英文中意为倒计数, Latch 为门问的意思.如果翻译成为倒计数门阀, 我想大家都会觉得不知所云吧! 因此,这里简单地称之为倒计数器.在这里, 门问的含义是:把门锁起来,不让里面的线程跑出来.因此,这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束, 再开始执行. CountDown Latch 的构造函数接收一个整数作为参数,即当前这个计数器的计数个数. public Co

  • php并发加锁问题分析与设计代码实例讲解

    在工作项目中,会遇到一些php并发访问去修改一个数据问题,如果这个数据不加锁,就会造成数据的错误.下面我将分析一个财务支付锁的问题.希望对大家有所帮助. 1 没有应用锁机制 1.1 财务支付简化版本代码 <!--?php /** * pay.php * * 支付没有应用锁 * * Copy right (c) 2016 * * modification history: * -------------------- * 2018/9/10, by CleverCode, Create * */

  • Java8的Stream()与ParallelStream()的区别说明

    Java8 Stream()与ParallelStream()区别 Stream 无状态:指元素的处理不受之前元素的影响: 有状态:指该操作只有拿到所有元素之后才能继续下去. 非短路操作:指必须处理所有元素才能得到最终结果: 短路操作:指遇到某些符合条件的元素就可以得到最终结果,如 A || B,只要A为true,则无需判断B的结果. ParallelStream 对于ParallelStream,需要知道的是里面的执行是异步的,并且使用的线程池是ForkJoinPool.common,可以通过

随机推荐