HashMap定时清理方法触发ConcurrentModificationException求助
嘿,我来帮你排查这个问题!你遇到的ConcurrentModificationException其实和你想的不一样——哪怕你的清理方法同一时间只执行一次,只要有其他线程在修改这个HashMap的结构(比如添加缓存时的put操作),就会触发这个异常。
为什么会报错?
HashMap的迭代器是fail-fast的:当迭代器创建后(比如你调用stream().min()时,内部会生成迭代器遍历entrySet),如果HashMap的结构发生了修改(比如put、remove、扩容),迭代器就会立刻抛出这个异常,不管修改操作是来自当前线程还是其他线程。
你的情况很可能是:清理方法在执行stream().min()遍历缓存时,另一个线程正在往cache里添加新的缓存文件(调用put),这就触发了迭代器的fail-fast机制,导致报错。
解决方案
这里有两种靠谱的解决思路,你可以根据自己的场景选择:
1. 换成线程安全的ConcurrentHashMap
ConcurrentHashMap的迭代器是弱一致的,它不会因为并发的结构修改而抛出异常,而是会遍历修改前的快照(或者尽可能新的状态),完美适配多线程读写的场景。
只需要把缓存的声明改成:
private ConcurrentHashMap<String, CachedFile> cache = new ConcurrentHashMap<>();
这种方式不需要额外加锁,代码改动最小。
2. 用同步块保证操作的原子性
如果你不想替换HashMap,可以给所有涉及cache的操作加锁,保证同一时间只有一个线程能修改或遍历它:
修改你的清理方法,把整个逻辑包裹在synchronized块里:
public void deleteFileCache() { synchronized (cache) { // 删除12小时未访问的文件 cache.entrySet().stream() .filter(entry -> LocalDateTime.now().minus(12, ChronoUnit.HOURS).isAfter(entry.getValue().getLastAccessed())) .forEach(entry -> { File file = new File(tempFolder, entry.getKey()); file.delete(); }); // 移除HashMap中对应的条目 cache.entrySet().removeIf(entry -> LocalDateTime.now().minus(12, ChronoUnit.HOURS).isAfter(entry.getValue().getLastAccessed())); // 剩余空间不足时删除最旧的文件 long freeSpace = tempFolder.getFreeSpace(); while (freeSpace < 6000000000L) { Optional<String> fileToDelete = cache.entrySet().stream() .min(Comparator.comparing(entry -> entry.getValue().getLastAccessed())) .map(Map.Entry::getKey); if (fileToDelete.isEmpty()) { break; // 没有缓存文件可删了,退出循环 } String filename = fileToDelete.get(); new File(tempFolder, filename).delete(); cache.remove(filename); freeSpace = tempFolder.getFreeSpace(); } } }
同时,所有其他修改cache的地方(比如添加缓存的put方法)也要用同一个锁对象同步:
public void addCachedFile(String key, CachedFile file) { synchronized (cache) { cache.put(key, file); } }
这样就能确保遍历和修改操作不会同时发生,彻底解决并发修改异常的问题。
内容的提问来源于stack exchange,提问作者isADon

