使用Java Stream未修改集合却抛出ConcurrentModificationException问题排查
多线程环境下HashMap的ConcurrentModificationException问题
我在多线程应用里偶尔会碰到ConcurrentModificationException,但堆栈跟踪指向的代码并未对集合执行任何修改操作:
Map<String, ProblemItem> problemItemsMap = getProblemItemsMap(); Optional<ProblemItem> findAny = problemItemsMap.values().stream() .filter(pi -> pi.getId().equals(id)) .findAny();
通过资料了解到这个异常和HashMap有关,当前代码没有执行添加、删除等修改操作,推测是其他线程同时修改了该Map导致的。报错堆栈如下:
Uncaught exception in thread 'pool-10-thread-1'.java.util.ConcurrentModificationException: null at java.base/java.util.HashMap$ValueSpliterator.tryAdvance(HashMap.java:1698) at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:127) at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:502) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:488) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.findAny(ReferencePipeline.java:548)
困惑的是,堆栈跟踪只显示迭代相关的代码,无法定位到修改操作的具体位置。
问题原因及解决办法
核心原因:HashMap是非线程安全集合,当一个线程通过流(本质是迭代)遍历其values时,若另一个线程对Map执行增、删、改操作,HashMap的fail-fast机制会直接抛出
ConcurrentModificationException,且异常仅在迭代线程抛出,修改线程不会留下堆栈痕迹。定位修改点的方法:
- 自定义HashMap子类,重写
put、remove、clear等修改方法,在方法内打印当前线程名及调用栈日志,记录所有修改操作的来源。 - 利用IDE条件断点:在HashMap的
put、remove方法中设置断点,条件设为当前线程名称不等于抛出异常的线程名(如!Thread.currentThread().getName().equals("pool-10-thread-1")),精准捕捉其他线程的修改行为。 - 替换为线程安全集合:直接用
ConcurrentHashMap替代HashMap,它采用弱一致性迭代器,不会触发fail-fast抛出异常,同时原生支持多线程安全操作,从根源避免问题。
- 自定义HashMap子类,重写
临时验证方案:
可以先将problemItemsMap包装为Collections.synchronizedMap(new HashMap<>()),虽然它的迭代仍会触发fail-fast,但能快速验证是否确实是其他线程修改导致的异常。不过该方案性能不如ConcurrentHashMap,仅适合临时排查。
内容的提问来源于stack exchange,提问作者Sadık
相关产品推荐
相关产品推荐

