
本文详解 `concurrentmodificationexception` 在遍历 `hashmap` 期间调用 `put()` 时的成因,并提供线程安全、逻辑正确的解决方案,包括 `putifabsent()` 替代方案及更优的统计实现。
你遇到的 ConcurrentModificationException 并非源于多线程并发——而是在单线程中对 HashMap 进行结构修改(如 put)的同时,正使用增强 for 循环(即 for (String key : letters.keySet()))遍历其键集,这会触发 fail-fast 机制。
根本原因在于:增强 for 循环底层依赖 Iterator,而 HashMap 的迭代器在创建时会记录内部结构修改次数(modCount)。一旦在迭代过程中调用 put()、remove() 等改变 HashMap 结构的方法,modCount 发生变化,下一次调用 next() 时就会检测到不一致,立即抛出 ConcurrentModificationException。
你的原始代码存在双重问题:
- 结构修改与迭代冲突:内层 for (String key : letters.keySet()) 正在遍历时,外层循环中 i 变化可能导致 letters.put(...) 被多次执行(尤其当新字符首次出现时),直接破坏迭代一致性;
- 逻辑冗余且低效:对每个 input.charAt(i) 都遍历整个 letters 键集,时间复杂度达 O(n²),且 containsKey() + put() 组合本可由原子操作替代。
✅ 正确解法:避免边遍历边修改。最简洁、语义清晰的方式是——先收集所有待处理字符,再统一构建或更新映射。例如,使用 putIfAbsent()(Java 8+):
Mapletters = new HashMap<>(); // 初始化首字符(非必需,可省略) letters.put(String.valueOf(input.charAt(0)), numberOfLettersInWord(input, input.charAt(0))); // 改为单层遍历:对每个字符尝试“仅当不存在时才插入” for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); String key = String.valueOf(c); letters.putIfAbsent(key, numberOfLettersInWord(input, c)); }
putIfAbsent(key, value) 是原子操作:若 key 不存在,则插入并返回 null;否则不修改并返回已有值。它既避免了 containsKey() + put() 的竞态窗口,又彻底消除了遍历中修改的场景。
? 进阶优化建议(推荐):
你实际需求是「统计字符串中各字母频次」,而 numberOfLettersInWord(input, c) 每次都全量扫描 input,导致整体时间复杂度高达 O(n²)。更高效的做法是一次遍历完成计数:
MapletterCount = new HashMap<>(); for (char c : input.toCharArray()) { letterCount.merge(c, 1, Integer::sum); // 或 letterCount.compute(c, (k, v) -> v == null ? 1 : v + 1); } // 若需 String 键,最后转换:letters = letterCount.entrySet().stream() // .collect(Collectors.toMap(e -> String.valueOf(e.getKey()), Map.Entry::getValue));
⚠️ 注意事项:
- 不要试图用 synchronized 或 Collections.synchronizedMap() 解决此问题——这是单线程逻辑错误,加锁无意义;
- ConcurrentHashMap 虽支持并发修改,但其迭代器仍是弱一致性(可能看不到最新修改),且在此场景下属于过度设计,反而增加复杂度;
- 始终牢记:任何基于 Collection.iterator() 的遍历(包括增强 for、forEach()、Stream 中间操作)期间,都不应调用会改变集合结构的方法(add/remove/put 等)。
总结:ConcurrentModificationException 是 JVM 对非法迭代修改的主动防护。修复核心在于分离「读取逻辑」与「修改逻辑」——先判定,后操作;或采用原子方法(如 putIfAbsent、merge)一步到位。你的重构已正确抓住关键,而进一步优化频次统计算法,将使代码兼具健壮性与高性能。










