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
- http://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html
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(); } });}
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 喝茶容易犯的两大误区,喝茶的三大误区
- 喝茶对胃有什么影响,饭后喝茶易贫血
- 红茶种植方法是什么,简易奶茶做法是什么
- 女性打篮球容易减肥吗
- 卫生间和厨房漏水是容易漏财的家居风水禁忌
- 古树茶与大树茶的区别,易武古茶园大树茶
- 内裤裆发黄是怎么回事
- 古法工艺密云龙,2013年八角亭易武古树茶王新品上市
- 什么运动容易减肥呢?
- 腹部减肥茶越喝越苗条,新鲜茶叶易伤肠胃
