Java8并行流用Collectors.toSet()收集抛出ConcurrentModificationException如何解决?
错误原因分析
- 异常触发的位置并非收集阶段,从堆栈
ArrayList$ArrayListSpliterator.forEachRemaining可以看出,错误发生在流读取源集合元素的遍历阶段,是Java集合的fail-fast快速失败机制触发的。 - 触发条件:你使用的
initialSet底层是普通非线程安全的集合实现(从堆栈看底层关联了ArrayList实现,大概率是你声明的Set实际运行时的实现类本身不支持并发遍历修改),在流处理(包括并行流、串行流)遍历源集合的过程中,源集合的结构(元素数量)发生了修改(add/remove操作),集合的modCount计数发生变化,遍历过程中检测到modCount和预期不一致就会抛出ConcurrentModificationException。 - 你使用的
parallelStream并行流本身不会导致这个问题,但是并行流场景下如果其他线程同时修改源集合,触发fail-fast的概率会比串行流更高。另外Java流的collect操作本身是线程安全的:并行流收集时每个线程会独立创建中间收集容器,最后合并结果,收集阶段不会修改源集合,也不会触发这个异常。
解决方案
- 先排查流处理过程中的修改操作:
- 检查filter、map等中间操作的lambda代码里,有没有直接修改
initialSet的add/remove逻辑 - 排查是否有其他业务线程在流执行的同时,对
initialSet做结构修改
- 检查filter、map等中间操作的lambda代码里,有没有直接修改
- 如果业务上确实需要流处理和源集合修改并发执行,把
initialSet替换为线程安全的Set实现:- 读多写少场景用
CopyOnWriteArraySet - 通用并发场景用
ConcurrentHashMap.newKeySet()
这两个实现都关闭了fail-fast检查,并发遍历修改不会抛出该异常
- 读多写少场景用
- 不需要并发处理的场景,可先对源集合做快照再执行流处理,避免源集合修改影响遍历,同时可优化原有lambda为方法引用写法,代码更简洁:
// 先复制生成源集合的快照,后续操作都基于快照执行 Set<Object> snapshot = new HashSet<>(initialSet); Set<Foo> filtered = snapshot.parallelStream() .filter(Foo.class::isInstance) .map(Foo.class::cast) .collect(Collectors.toSet());
内容的提问来源于stack exchange,提问作者LJ in NJ
相关产品推荐
相关产品推荐

