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

如何解决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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 01:28:16