好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

关于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对插入列表操作进行同步:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

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测试数据/questions/60401/using-java-8-parallel-streams https://stackoverflow测试数据/questions/22350288/parallel-streams-collectors-and-thread-safety

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

http://docs.oracle测试数据/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方法句柄:

?

1

<r, a> r collect(collector<? super t, a, r> collector);

在该实现方法中,参数是一个collector对象,可以使用collectors类的静态方法构造collector对象,比如collectors.tolist(),toset(),tomap(),etc,这块很容易查到api故不细说了。

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

?

1

2

3

4

5

supplier<a> supplier();

biconsumer<a, t> accumulator();

binaryoperator<a> combiner();

function<a, r> finisher();

set<characteristics> characteristics();

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

上图来自:http://HdhCmsTestinfoq测试数据/cn/articles/fork-join-introduction

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

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

compute.java:

?

1

2

3

4

5

public class compute {

public double compute( int num) {

  return ( double ) ( 2 * num);

}

}

接下来在main.java中定义输入的类型为arraylist的nums和类型为set的输出结果result:

?

1

2

private list<integer> nums = new arraylist<>();

private set< double > result = new hashset<>();

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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

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:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class container {

  // 定义本地的result

  public set< double > set;

 

  public container() {

  this .set = new hashset<>();

  }

 

  public container accumulate( int num) {

  this .set.add(compute测试数据pute(num));

  return this ;

  }

 

  public container combine(container container) {

  this .set.addall(container.set);

  return this ;

  }

 

  public set< double > getresult() {

  return this .set;

  }

}

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

?

1

2

3

4

5

6

7

8

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

总结

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

原文链接:https://HdhCmsTestcnblogs测试数据/puyangsky/p/7608741.html

查看更多关于关于Java8 parallelStream并发安全的深入讲解的详细内容...

  阅读:34次