Java/Kotlin HashMap 等集合引发 ConcurrentModificationException

在对一些非并发集合同时进行读写的时候,会抛出 ConcurrentModificationException

异常产生示例

示例一(单线程): 遍历集合时候去修改

抛出 ConcurrentModificationException 的主要原因是当你在遍历一个集合(如 Map、List 或 Set)时,同时对该集合进行了结构性修改(例如添加或删除元素),而这些修改没有通过迭代器自身的相应方法进行。

java 复制代码
    private static Map<String, String> map = new HashMap<>();

    static {
        for (int i = 0; i < 100000; i++) {
            map.put(String.valueOf(i), String.valueOf(i));
        }
    }

    public static void main(String[] args) {
        // 在循环的时候删除元素
        map.forEach((key, value) -> {
            if (Integer.parseInt(key) % 2 == 0){
                map.remove(key);
                return;
            }
            System.out.println(key + value);
        });
    }

示例二(多线程): 多线程读写

多线程读写和示例一抛出异常的原因一样

java 复制代码
 private static Map<String, String> map = new HashMap<>();

    static {
        for (int i = 0; i < 100000; i++) {
            map.put(String.valueOf(i), String.valueOf(i));
        }
    }

    public static void main(String[] args) {
        // 并发修改 map 让其出现并发异常
        for (int i = 0; i < 100; i++) {
            new Thread(new Add(map)).start();
            new Thread(new Read(map)).start();
        }
    }

    static class Add implements Runnable {
        private HashMap<String, String> map;

        public Add(HashMap<String, String> map) {
            this.map = map;
        }

        @Override
        public void run() {
            map.clear();
            for (int i = 0; i < 100000; i++) {
                map.put(String.valueOf(i), String.valueOf(i));
            }
        }
    }

    static class Read implements Runnable {
        private HashMap<String, String> map;

        public Read(HashMap<String, String> map) {
            this.map = map;
        }

        @Override
        public void run() {
            CopyOnWriteArraySet<Map.Entry<String,String>> copyOnWriteArraySet = new CopyOnWriteArraySet<>(map.entrySet());
            for (int i = 0; i < 100000; i++) {
                copyOnWriteArraySet.forEach((entry) -> {
                    String a = entry.getKey() + entry.getValue();
                });
            }
        }
    }

解决问题

解决示例一

  1. CopyOnWriteArraySet

CopyOnWriteArraySet 是 Java 并发集合包中的一种线程安全的集合。它的关键特性在于"写时复制"(copy-on-write),这意味着在对集合进行修改操作(如添加或删除元素)时,其实现机制是创建底层数组的一个新副本,而不是直接在原数组上进行修改。这种机制使得在遍历 CopyOnWriteArraySet 时不会抛出 ConcurrentModificationException,因为迭代器是在数组的一个快照上工作的,它不受后续对集合进行的修改的影响。

java 复制代码
CopyOnWriteArraySet<Map.Entry<String,String>> copyOnWriteArraySet = new CopyOnWriteArraySet<>(map.entrySet());
copyOnWriteArraySet.forEach((entry) -> {
    String key = entry.getKey();
    String value = entry.getValue();
    if (Integer.parseInt(key) % 2 == 0){
        map.remove(key);
        return;
    }
    System.out.println(key + value);
});
  1. 迭代器

当使用迭代器遍历集合时,直接调用集合的 remove 方法(如 map.remove(key))会导致 ConcurrentModificationException,因为这破坏了迭代器预期的遍历顺序。而迭代器自身提供的 remove 方法是唯一一种可以在遍历过程中安全移除元素的方法。该方法确保了在删除元素后,迭代器能够继续正确地跟踪集合的状态,不会导致并发修改异常。

java 复制代码
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, String> entry = iterator.next();
    if (Integer.parseInt(entry.getKey()) % 2 == 0) {
        iterator.remove(); // 使用迭代器的remove方法来安全地移除元素
    } else {
        System.out.println(entry.getKey() + entry.getValue());
    }
}
  1. 替换原本的集合
java 复制代码
map = map.entrySet().stream()
        .filter(entry -> Integer.parseInt(entry.getKey()) % 2 != 0)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v1, HashMap::new));
  1. 包装map
java 复制代码
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>(map);
concurrentHashMap.forEach((key, value) -> {
    if (Integer.parseInt(key) % 2 == 0){
        map.remove(key);
        return;
    }
    System.out.println(key + value);
});

方式二,也是替换 Map

java 复制代码
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>(map);
concurrentHashMap.forEach((key, value) -> {
    if (Integer.parseInt(key) % 2 == 0){
        concurrentHashMap .remove(key);
        return;
    }
    System.out.println(key + value);
});
map = concurrentHashMap 
  1. 使用 removeIf
java 复制代码
 map.entrySet().removeIf(f-> Integer.parseInt(f.getKey()) % 2 == 0);

解决示例二

HashMap 本身并不是线程安全的,最直接有效的方法是直接还一个线程安全的集合

Java 提供了多种线程安全的集合,它们主要通过不同的方式来支持并发操作。以下是一些常见的线程安全集合:


1. 基于 java.util.concurrent 包的线程安全集合

这些集合在高并发场景下性能较好,适用于大多数现代应用。

1.1 ConcurrentHashMap
  • 特性:线程安全的哈希表,支持高效的并发读写。

  • 优势:读操作无锁,写操作基于分段锁(Java 8 后使用 CAS)。

  • 用法:

    java 复制代码
    ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
1.2 CopyOnWriteArrayList
  • 特性:在写操作时复制整个底层数组,因此适合读多写少的场景。

  • 优势:读操作无锁,写操作创建新数组,避免并发问题。

  • 用法:

    java 复制代码
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
1.3 CopyOnWriteArraySet
  • 特性:基于 CopyOnWriteArrayList 实现,适合高频读取、低频修改的场景。

  • 用法:

    java 复制代码
    CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
1.4 ConcurrentLinkedQueue
  • 特性:基于链表的无界非阻塞线程安全队列。

  • 优势:采用 CAS 操作,适合高并发环境下的队列操作。

  • 用法:

    java 复制代码
    ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
1.5 ConcurrentLinkedDeque
  • 特性:双端队列版本的 ConcurrentLinkedQueue,支持高效的双向操作。

  • 用法:

    java 复制代码
    ConcurrentLinkedDeque<String> deque = new ConcurrentLinkedDeque<>();
1.6 LinkedBlockingQueue
  • 特性:基于链表的阻塞队列,支持可选的容量限制。

  • 优势:适合生产者-消费者模型。

  • 用法:

    java 复制代码
    LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(100);
1.7 LinkedBlockingDeque
  • 特性:双端队列版本的 LinkedBlockingQueue,支持从两端进行操作。

  • 用法:

    java 复制代码
    LinkedBlockingDeque<String> deque = new LinkedBlockingDeque<>(100);
1.8 ConcurrentSkipListMap
  • 特性:基于跳表的线程安全 SortedMap 实现,支持按键排序。

  • 用法:

    java 复制代码
    ConcurrentSkipListMap<String, String> map = new ConcurrentSkipListMap<>();
1.9 ConcurrentSkipListSet
  • 特性:基于 ConcurrentSkipListMap 实现的线程安全有序集合。

  • 用法:

    java 复制代码
    ConcurrentSkipListSet<String> set = new ConcurrentSkipListSet<>();

2. 基于 Collections 工具类的同步集合

Collections.synchronizedXXX 方法可以将非线程安全的集合包装成线程安全集合,但性能不如 java.util.concurrent 包。

2.1 SynchronizedList
  • 特性:将普通 List 包装成线程安全集合。

  • 用法:

    java 复制代码
    List<String> list = Collections.synchronizedList(new ArrayList<>());
2.2 SynchronizedSet
  • 特性:将普通 Set 包装成线程安全集合。

  • 用法:

    java 复制代码
    Set<String> set = Collections.synchronizedSet(new HashSet<>());
2.3 SynchronizedMap
  • 特性:将普通 Map 包装成线程安全集合。

  • 用法:

    java 复制代码
    Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

注意 :使用 Collections.synchronizedXXX 包装的集合在迭代时需要显式加锁:

java 复制代码
synchronized (list) {
    for (String s : list) {
        // 操作
    }
}

3. Immutable Collections(不可变集合)

Java 9 引入了不可变集合,通过 List.of()Set.of()Map.of() 创建:

  • 特性:线程安全,不可修改,适合配置类或常量类数据。

  • 用法:

    java 复制代码
    List<String> list = List.of("a", "b", "c");
    Set<String> set = Set.of("a", "b", "c");
    Map<String, String> map = Map.of("key1", "value1", "key2", "value2");

4. Vector 和 Stack

这些是早期的线程安全集合:

  • Vector :线程安全的 List 实现。

    java 复制代码
    Vector<String> vector = new Vector<>();
  • Stack :线程安全的栈(继承自 Vector)。

    java 复制代码
    Stack<String> stack = new Stack<>();

但它们性能较低,通常不推荐使用。


推荐选择

  1. 高并发场景 :优先使用 java.util.concurrent 包下的集合,例如 ConcurrentHashMapCopyOnWriteArrayListCopyOnWriteArraySet等。
  2. 读多写少 :使用 CopyOnWriteArrayListCopyOnWriteArraySet
  3. 简易同步 :使用 Collections.synchronizedXXX 包装集合。
  4. 不可修改数据 :使用不可变集合(List.of 等)。

如果你有特定的应用场景,可以详细讨论选择最优集合!

相关推荐
程序猿大波1 小时前
基于Java,SpringBoot,Vue,HTML高校社团信息管理系统设计
java·vue.js·spring boot
小李同学_LHY2 小时前
微服务架构中的精妙设计:环境和工程搭建
java·spring·微服务·springcloud
慕容魏2 小时前
面经分享,中科创达(安卓开发,二面挂)
java·开发语言
不辉放弃2 小时前
Java/Scala是什么
java·scala
喵手2 小时前
Java实现视频格式转换的完整指南:从FFmpeg到纯Java方案!
java·开发语言·ffmpeg
天上掉下来个程小白2 小时前
Redis-04.Redis常用命令-字符串常用命令
java·数据库·redis·springboot·苍穹外卖
Zz_waiting.3 小时前
多线程 - 线程安全 2 -- > 死锁问题
java·开发语言
就改了3 小时前
Java进阶——Lombok的使用
java·服务器·前端
Agome993 小时前
linux面试题
java·开发语言·excel
故事与他6453 小时前
电子文档安全管理系统V6.0接口backup存在任意文件下载漏洞
java·开发语言·前端·javascript·安全·网络安全