You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程单元测试加同步后仍抛出ConcurrentModificationException的原因

异常根本原因

这个异常和多线程并发访问没有关系,是clearMaterialFromCache方法内的迭代删除逻辑违反了HashMap的使用规则导致的:

  • 你在同步块内用for-each遍历materialsCache.keySet()时,本质是通过HashMap自带的KeyIterator做迭代。HashMap的迭代器内部会维护一个expectedModCount值,记录迭代器创建时Map的结构修改次数(modCount),每次调用next()获取下一个元素时,都会校验当前Map的modCount是否和expectedModCount一致,不一致就会抛出ConcurrentModificationException。
  • 你在迭代过程中没有通过迭代器提供的remove()方法删除元素,而是直接调用materialsCache.remove(materialSpec)修改Map结构,这个操作会让Map的modCount自增,和迭代器记录的expectedModCount不匹配,触发异常。整个异常触发流程全程在当前持有锁的单线程内完成,和其他线程的访问无关。
修复方式

两种常用的正确实现:

  • 方式1:显式获取迭代器,调用迭代器的remove()方法删除元素
public void clearMaterialFromCache(String materialId) {
    synchronized (LOCK) {
        Iterator<MaterialSpec> keyIterator = materialsCache.keySet().iterator();
        while (keyIterator.hasNext()) {
            MaterialSpec spec = keyIterator.next();
            if (materialId.equals(spec.getMaterialId())) {
                keyIterator.remove();
            }
        }
    }
}
  • 方式2:Java 8及以上版本可以直接调用集合的removeIf方法,底层已经封装了迭代器删除逻辑,代码更简洁
public void clearMaterialFromCache(String materialId) {
    synchronized (LOCK) {
        materialsCache.keySet().removeIf(spec -> materialId.equals(spec.getMaterialId()));
    }
}
额外注意点

你当前getAndCacheMaterial的双检锁实现存在缺陷:两次同步块中间调用materialProvider.getMaterial(materialSpec)时没有持有锁,多线程场景下可能出现多个线程同时判断缓存不存在,重复调用materialProvider查询相同spec的问题,还可能出现缓存对象未完全构造就被其他线程读取的问题。如果需要更可靠的线程安全缓存实现,可以直接使用ConcurrentHashMap配合computeIfAbsent方法,不需要手动加锁就能实现线程安全的懒加载缓存。

内容的提问来源于stack exchange,提问作者Vasiliy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 10:15:30