如何解决Java中的java.util.ConcurrentModificationException异常?
问题原因分析
你的代码抛出java.util.ConcurrentModificationException主要有两个核心原因:
HashMap非线程安全:多个线程同时对HashMap执行put操作时,会破坏其内部哈希表结构,触发并发修改异常。- 主线程未等待子线程执行完毕:主线程启动所有子线程后立刻遍历
map,此时子线程可能还在修改map,而HashMap的迭代器是快速失败(fail-fast)机制,一旦检测到迭代过程中集合被修改,就会抛出异常。
解决方案
下面提供几种可行的修复方案:
方案1:使用线程安全的ConcurrentHashMap+等待子线程执行完毕
这是最推荐的方案,兼顾线程安全与并发性能:
package thread; import java.util.Map.Entry; import java.util.Scanner; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; // 修正类名符合Java命名规范 class Occurrence extends Thread { private final String s; private final ConcurrentHashMap<Character, Integer> map; private final CountDownLatch latch; Occurrence(String s, ConcurrentHashMap<Character, Integer> map, CountDownLatch latch) { this.s = s; this.map = map; this.latch = latch; } @Override public void run() { try { for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); // 原子操作更新计数,避免多线程下计数丢失 map.merge(c, 1, Integer::sum); } } finally { // 线程执行完毕后计数器减1 latch.countDown(); } } } class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); ConcurrentHashMap<Character, Integer> map = new ConcurrentHashMap<>(); // 用CountDownLatch等待所有子线程完成 CountDownLatch latch = new CountDownLatch(n); for (int i = 0; i < n; i++) { String s = sc.next(); Occurrence o = new Occurrence(s, map, latch); o.start(); } try { // 阻塞主线程,直到所有子线程执行完毕 latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 所有线程完成后再遍历输出 for (Entry<Character, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } sc.close(); } }
关键修改点:
- 替换
HashMap为ConcurrentHashMap,这是Java提供的线程安全哈希表实现,支持并发读写。 - 使用
CountDownLatch实现主线程对所有子线程的等待,避免遍历与修改操作并发执行。 - 使用
merge()方法实现原子性计数更新,解决原代码中get+put非原子操作导致的计数丢失问题。 - 修正类名为
Occurrence,符合Java类命名规范。
方案2:用同步代码块保护HashMap操作
如果必须使用HashMap,可以通过同步代码块保证同一时间只有一个线程操作map:
package thread; import java.util.Map.Entry; import java.util.HashMap; import java.util.Scanner; import java.util.concurrent.CountDownLatch; class Occurrence extends Thread { private final String s; private final HashMap<Character, Integer> map; private final CountDownLatch latch; Occurrence(String s, HashMap<Character, Integer> map, CountDownLatch latch) { this.s = s; this.map = map; this.latch = latch; } @Override public void run() { try { for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); // 同步代码块保证map操作的原子性 synchronized (map) { if (map.containsKey(c)) { map.put(c, map.get(c) + 1); } else { map.put(c, 1); } } } } finally { latch.countDown(); } } } class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); HashMap<Character, Integer> map = new HashMap<>(); CountDownLatch latch = new CountDownLatch(n); for (int i = 0; i < n; i++) { String s = sc.next(); Occurrence o = new Occurrence(s, map, latch); o.start(); } try { latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 遍历map时也加同步,避免并发问题 synchronized (map) { for (Entry<Character, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } } sc.close(); } }
关键修改点:
- 用
synchronized (map)包裹所有对map的修改和遍历操作,保证同一时间只有一个线程操作map。 - 同样使用
CountDownLatch等待所有子线程完成后再执行遍历。
方案3:用Java 8并行流简化实现
如果不需要手动管理线程,可以用并行流简化代码,底层自动处理线程安全:
package thread; import java.util.Map; import java.util.Scanner; import java.util.stream.Collectors; class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); Map<Character, Integer> result = new java.util.HashMap<>(); for (int i = 0; i < n; i++) { String s = sc.next(); // 并行处理字符串字符,统计次数 s.chars() .mapToObj(c -> (char) c) .collect(Collectors.groupingByConcurrent(c -> c, Collectors.summingInt(c -> 1))) .forEach((k, v) -> result.merge(k, v, Integer::sum)); } result.forEach((k, v) -> System.out.println(k + ": " + v)); sc.close(); } }
说明:
groupingByConcurrent返回线程安全的ConcurrentHashMap,并行流自动处理多线程统计,最后用merge合并结果,避免线程安全问题。
总结
优先选择方案1,ConcurrentHashMap结合CountDownLatch的方式既保证线程安全,又有较好的并发性能;方案2适合必须使用HashMap的简单场景,但并发性能较低;方案3是更简洁的现代Java写法,无需手动管理线程。
内容的提问来源于stack exchange,提问作者creksooscrec
相关产品推荐
相关产品推荐

