当前位置:首页 > java技术文章 > Java8 parallelStream并发安全

Java8 parallelStream并发安全原理讲解

  • 发布时间:
  • 作者:码农之家原创
  • 点击:586

这篇文章主要知识点是关于java、stream使用、java、stream、并行、java8、parallelstream、java8中parallelStream性能测试及结果分析 的内容,如果大家想对相关知识点有系统深入的学习,可以参阅以下电子书

Java数字图像处理:编程技巧与应用实践
  • 类型:Java编程大小:153.4 MB格式:PDF作者:贾志刚
立即下载
Java语言程序设计教程
  • 类型:Java大小:55.5 MB格式:PDF作者:朱晓龙
立即下载

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是怎么运转的,一图以蔽之:

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

上图来自: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操作就是满足线程安全的了。

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 class StreamBenchTest {
  List<String> data = new ArrayList<>();
  @Setup
  public void init() {
    // prepare
    for(int i=0;i<100;i++){
      data.add(UUID.randomUUID().toString());
    }
  }
  @TearDown
  public void destory() {
    // destory
  }
  @Benchmark
  public void benchStream(){
    data.stream().forEach(e -> {
      e.getBytes();
      try {
        Thread.sleep(10);
      } catch (InterruptedException e1) {
        e1.printStackTrace();
      }
    });
  }
  @Benchmark
  public void benchParallelStream(){
    data.parallelStream().forEach(e -> {
      e.getBytes();
      try {
        Thread.sleep(10);
      } catch (InterruptedException e1) {
        e1.printStackTrace();
      }
    });
  }
  public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder()
        .include(".*" +StreamBenchTest.class.getSimpleName()+ ".*")
        .forks(1)
        .build();
    new Runner(opt).run();
  }
}

parallelStream线程数

默认是Runtime.getRuntime().availableProcessors() - 1,这里为7

运行结果

# Run complete. Total time: 00:02:44
Benchmark              Mode Cnt      Score     Error Units
StreamBenchTest.benchParallelStream avgt  20  155868805.437 ± 1509175.840 ns/op
StreamBenchTest.benchStream     avgt  20 1147570372.950 ± 6138494.414 ns/op

测试2

将数据data改为30,同时sleep改为100

Benchmark              Mode Cnt      Score     Error Units
StreamBenchTest.benchParallelStream avgt  20  414230854.631 ± 725294.455 ns/op
StreamBenchTest.benchStream     avgt  20 3107250608.500 ± 4805037.628 ns/op

可以发现sleep越长,parallelStream优势越明显。

小结

parallelStream在阻塞场景下优势更明显,其线程池个数默认为
Runtime.getRuntime().availableProcessors() - 1,如果需修改则需设置-Djava.util.concurrent.ForkJoinPool.common.parallelism=8

以上就是本次讲述知识点的全部内容,感谢你对码农之家的支持。

以上就是本次给大家分享的关于java的全部知识点内容总结,大家还可以在下方相关文章里找到相关文章进一步学习,感谢大家的阅读和支持。

推荐内容

idea2020注册激活码(激活到2100年)

实例分析Java实现的zip压缩及解压缩工具类

python3 pandas 如何读取MySQL数据和插入

ThinkPHP3.2.3框架如何实现分页功能

深入理解JS函数stack size计算方法

展开 +

收起 -

Java8 相关电子书
学习笔记
网友NO.330109

java8新特性教程之time包使用总结

前言 Java8新特性java.time.*包学习。 自从java发布模式变更就发现自己有些跟不上他们的速度,java8还有不少没有用透而9、10、11相继出来,长江后浪推前浪一浪胜过一浪。之前date的使用还不敢自信说多透彻,后续都是泪...(欢迎酱油...) 以jdk1.8.0_111为例 新的设计思路 引入final定义支持时间点不可变和线程安全,长久来的Date的设计一直遭人诟病着; 设计LocalDate、LocalDateTime、LocalTime、instant、Clock、Duration等类,format\zoo\temporal等包规范时间的定义划分; 时间统一使用 ISO-8601 日历系统,也就是yyyy-MM-dd'T'HHss:SSSZZ格式,输出2012-04-13T10:53:43:119+08:00样子,要是用过jota-time包估计你什么都懂了; 规范并提供更加好用的时间操作方法,plus\minus\with\to\get\of\now等方法命名规则; jdk1.8包目录简介: time:父级基础包,常用的时间相关类都在这里,如LocalDate\LocalDateTime\Instan……

网友NO.613344

详解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 是一个收集函数的接口, 声明了一个收集器的功能。 java.util.Comparators则是一个收集器的工具类,内置了一系列收集器实现。 收集器的作用 你可以把Java8的流看做花哨又懒惰的数据集迭代器。他们支持两种类型的操作:中间操作(e.g. filter, map)和终端操作(如count, findFirst, forEach, reduce). 中间操作可以连接起来,将一个流转换为另一个流。这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗类,产生一个最终结果。collect就是一个归约操作,就像reduce一样可以接受各种做法作为参数,将……

网友NO.738196

Java8中Optional的一些常见错误用法总结

前言 Java 8 引入的 Optional 类型,基本是把它当作 null 值优雅的处理方式。其实也不完全如此,Optional 在语义上更能体现有还是没有值。所以它不是设计来作为 null 的替代品,如果方法返回 null 值表达了二义性,没有结果或是执行中出现异常。 在 Oracle 做 Java 语言工作的 Brian Goetz 在 Stack Overflow 回复 Should Java 8 getters return optional type? 中讲述了引入 Optional 的主要动机。 Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result”, and using null for such was overwhelmingly likely to cause errors. 说的是 Optional 提供了一个有限的机制让类库方法返回值清晰的表达有与没有值,避免很多时候 null 造成的错误。并非有了 Optional 就要完全杜绝 NullPointerException。 在 Java 8 之前一个实践是方法返回集合或数组时,应返回空集合或数组……

网友NO.666709

java8新特性之Optional的深入解析

前言 最近脑袋发热追着java8源码看的很起劲,还有了执念,罪过。 本文以jdk1.8.0_111源码为例 public final class OptionalT {} Optional是一个为了解决NullPointerException设计而生可以包含对象也可以包含空的容器对象。封装了很多对空处理的方法也增加了filter、map这样的检索利器,其中函数式编程会有种炫酷到爆的感觉。 基础测试用例对象: public class Java8OptionalTest { ListString stringList = null; ICar car = new WeiLaiCar();}public class WeiLaiCar implements ICar { Integer wheels = new Integer(4);} Api中提供的4种optional 最核心的当属Optional对象,泛型的引入支持了所有对象类型,又增加对常用场景下的dubbo\int\long进行扩展。重点介绍一下Optional对象的方法其他三个类似。 public final class OptionalT { public final class OptionalDouble { public final class OptionalInt { public final class OptionalLong { @FunctionalInterface Predicate\C……

<
1
>

电子书 编程教程 文档 软件 源码 视频

Copyright 2018-2020 xz577.com 码农之家

本站所有电子书资源不再提供下载地址,只分享来路

免责声明:网站所有作品均由会员网上搜集共同更新,仅供读者预览及学习交流使用,下载后请24小时内删除

版权投诉 / 书籍推广 / 赞助:QQ:520161757