【Java】关于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操作就是满足线程安全的了。
【Java】关于Java8 parallelStream并发安全的思考的更多相关文章
- Java8高中并发
Java8中学并发 本文翻译自:http://jaxenter.com/lean-concurrency-in-java-8-49924.html 转载请注明出处:http://blog.csdn.n ...
- java swing窗口放置屏幕中央问题思考
java swing窗口放置屏幕中央问题思考 以前总是尝试各种方法都没有能把组件放到屏幕中央,只能用死办法,设置绝对坐标,但这样就失去了可移植性,而且繁琐.今天仔细思考了一番,终于被我找出问题所在. ...
- Java 理论与实践: 并发集合类
Java 理论与实践: 并发集合类 DougLea的 util.concurrent 包除了包含许多其他有用的并发构造块之外,还包含了一些主要集合类型 List 和 Map 的高性能的.线程安全的实现 ...
- java线程安全之并发Queue
关闭 原 java线程安全之并发Queue(十三) 2017年11月19日 23:40:23 小彬彬~ 阅读数:12092更多 所属专栏: 线程安全 版权声明:本文为博主原创文章,未经博主允许不 ...
- Java 面试宝典!并发编程 71 道题及答案全送上!
金九银十跳槽季已经开始,作为 Java 开发者你开始刷面试题了吗?别急,我整理了71道并发相关的面试题,看这一文就够了! 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程( ...
- JAVA系统架构高并发解决方案 分布式缓存 分布式事务解决方案
JAVA系统架构高并发解决方案 分布式缓存 分布式事务解决方案
- Java多线程专题1: 并发与并行的基础概念
合集目录 Java多线程专题1: 并发与并行的基础概念 什么是多线程并发和并行? 并发: Concurrency 特指单核可以处理多任务, 这种机制主要实现于操作系统层面, 用于充分利用单CPU的性能 ...
- 《深入了解java虚拟机》高效并发读书笔记——Java内存模型,线程,线程安全 与锁优化
<深入了解java虚拟机>高效并发读书笔记--Java内存模型,线程,线程安全 与锁优化 本文主要参考<深入了解java虚拟机>高效并发章节 关于锁升级,偏向锁,轻量级锁参考& ...
- Java8 parallelStream浅析
JAVA8中引入了lamda表达式和Stream接口.其丰富的API及强大的表达能力极大的简化代码,提升了效率,同时还通过parallelStream提供并发操作的支持,本文探讨parallelStr ...
随机推荐
- 最强黑吃黑:WEBSHELL大马隐藏万能密码大全
因为很多原因,很多新手都不会编写自己的大马,大多数新手都会通过百度去下载对应脚本的大马,然而这些webshell大马都是早期流传出来的,基本上都存在后门,可以通过万能密码登录,即使你修改i过密码了,怎 ...
- maven 搭新建成之后 无法创建 src/main/java 目录解决
maven项目创建后 创建 src/main/java 和 src/main/test 会报错,目录已存在 打开build path 界面 src/main/java 和 ...
- shell之 printf 输出语句
总结: (1)printf 使用引用文本或空格分隔的参数,外面可以在printf中使用格式化字符串,还可以制定字符串的宽度.左右对齐方式等.默认printf不会像 echo 自动添加换行符,我们可以手 ...
- netcore2.0 ORM框架中如何配置自定义的主外键加载
环境:netcore2.0 DB :mysql ORM:Ant https://github.com/yuzd/AntData.ORM/tree/netcore2 [给我一个star吧] NUGET: ...
- 百度鹰眼Java接口调用增删改查实例
因感觉百度鹰眼的使用场景比较符合实际业务,于是对百度鹰眼做了简单功能调试.刚开始使用springframework封装的RestTemplate,但是测试提示ak参数不存在.后又试了几种方法,均提示a ...
- Ext.data.Store添加动态参数
多条件查询页面的参数都是动态的,并且我们通常还会有默认加载页面.此时,动态添加参数非常重要,其中baseparam是解决问题的关键. @ 将查询条件定义为一个全局变量 var param_01 = & ...
- xtream 示例介绍
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt271 1 xStream框架 xStream可以轻易的将Java对象和xml ...
- 【C++小白成长撸】--矩阵乘法程序
矩阵乘法是大学矩阵课程中,相比矩阵加减法比较困难的部分. 矩阵乘法的原理: 矩阵乘法在代码中实现 得到目标矩阵的一个元素,涉及两个求和符号,一个求和符号一个for循环,两个求和符号两个for循环,再加 ...
- 从web图片裁剪出发:了解H5中的Blob
刚开始做前端的时候,有个功能卡住我了,就是裁剪并上传头像.当时两个方案摆在我面前,一个是flash,我不会.另一个是通过iframe上传图片,然后再上传坐标由后端裁剪,而我最终的选择是后者.有人会疑惑 ...
- (三)、LNMP的搭建,并制作rpm包
中小型规模网站集群架构:yum仓库搭建 : 矮哥linux运维群:93324526 编译的三条命令的规则 ./configure 就是在本地创建了一个Makefile文件 (也就是指定一下各种配置参数 ...