多线程单元测试加同步后仍抛出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
相关产品推荐
相关产品推荐

