跳转至

深入解析 java.util.ConcurrentModificationException null

简介

在 Java 开发中,java.util.ConcurrentModificationException 是一个常见且令人头疼的异常。当我们在遍历集合(如 ArrayListHashMap 等)的过程中尝试对集合进行结构上的修改(例如添加、删除元素)时,就可能会抛出这个异常。而 java.util.ConcurrentModificationException null 则是这个异常在特定情况下带有 null 值的变体。理解这个异常的本质、产生原因以及如何处理它,对于编写健壮的 Java 代码至关重要。

目录

  1. 基础概念
  2. 异常产生原因
  3. 使用方法(实际并不存在“使用”,这里指深入理解)
  4. 常见实践(错误示例)
  5. 正确处理方式(最佳实践)
  6. 小结
  7. 参考资料

基础概念

java.util.ConcurrentModificationException 属于运行时异常(RuntimeException),它继承自 ConcurrentModificationException 类。这个异常通常出现在当一个迭代器(Iterator)检测到集合在迭代过程中被意外修改的情况。Java 集合框架采用了快速失败(fail - fast)机制,目的是在并发修改的情况下尽快抛出异常,以保证集合状态的一致性。

异常产生原因

Java 集合在迭代时会维护一个内部的版本号(modCount)。当集合结构发生变化(添加或删除元素)时,这个版本号会增加。迭代器在创建时会记录当前集合的版本号。在迭代过程中,每次调用 next()remove() 方法时,迭代器都会检查集合的当前版本号是否与它创建时记录的版本号相同。如果不同,就意味着集合在迭代过程中被修改了,此时会抛出 ConcurrentModificationException

使用方法(深入理解)

虽然我们不能“使用”这个异常来实现特定功能,但深入理解它有助于编写更健壮的代码。下面通过一个简单的代码示例来展示异常是如何产生的:

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 普通的 for - each 循环遍历
        for (String fruit : list) {
            if ("Banana".equals(fruit)) {
                list.remove(fruit); // 尝试在遍历过程中删除元素
            }
        }
    }
}

在上述代码中,我们在使用 for - each 循环遍历 ArrayList 的过程中尝试删除元素。运行这段代码,会抛出 ConcurrentModificationException。这是因为 for - each 循环底层使用的是迭代器,在删除元素时集合的 modCount 发生了变化,而迭代器检测到版本号不一致,从而抛出异常。

常见实践(错误示例)

错误示例 1:在普通 for - each 循环中删除元素

import java.util.ArrayList;
import java.util.List;

public class BadPractice1 {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);

        for (Integer number : numbers) {
            if (number == 2) {
                numbers.remove(number);
            }
        }
        System.out.println(numbers);
    }
}

运行上述代码,会抛出 ConcurrentModificationException。因为 for - each 循环使用迭代器进行遍历,在遍历过程中修改集合结构会导致版本号不一致。

错误示例 2:多线程环境下未正确同步

import java.util.ArrayList;
import java.util.List;

public class BadPractice2 {
    private static List<Integer> sharedList = new ArrayList<>();

    public static void main(String[] args) {
        sharedList.add(1);
        sharedList.add(2);
        sharedList.add(3);

        Thread thread1 = new Thread(() -> {
            for (Integer number : sharedList) {
                System.out.println("Thread 1: " + number);
            }
        });

        Thread thread2 = new Thread(() -> {
            sharedList.remove(2);
            System.out.println("Thread 2 removed an element");
        });

        thread1.start();
        thread2.start();
    }
}

在多线程环境中,如果没有正确同步对集合的访问,一个线程在遍历集合时,另一个线程修改集合,很可能会抛出 ConcurrentModificationException

正确处理方式(最佳实践)

方法 1:使用迭代器的 remove() 方法

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GoodPractice1 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String fruit = iterator.next();
            if ("Banana".equals(fruit)) {
                iterator.remove(); // 使用迭代器的 remove 方法
            }
        }
        System.out.println(list);
    }
}

这种方法使用迭代器的 remove() 方法,它会正确地更新集合的版本号,避免抛出 ConcurrentModificationException

方法 2:使用 Copy - on - Write 集合

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class GoodPractice2 {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        for (String fruit : list) {
            if ("Banana".equals(fruit)) {
                list.remove(fruit); // 这里不会抛出异常
            }
        }
        System.out.println(list);
    }
}

CopyOnWriteArrayList 是一个线程安全的集合,它在写操作时会复制底层数组,读操作使用旧的数组。因此,读操作不会受到写操作的影响,也就不会抛出 ConcurrentModificationException。不过,由于写操作需要复制数组,所以性能开销较大,适合读多写少的场景。

方法 3:使用并发集合类

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class GoodPractice3 {
    public static void main(String[] args) {
        ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);

        map.forEach((key, value) -> {
            if ("Two".equals(key)) {
                map.remove(key); // 不会抛出异常
            }
        });
        System.out.println(map);
    }
}

ConcurrentHashMap 是线程安全的哈希表,它允许在遍历的同时进行修改操作,通过内部的同步机制避免了 ConcurrentModificationException 的抛出。

小结

java.util.ConcurrentModificationException null 是在 Java 集合遍历过程中并发修改集合结构时可能出现的异常。理解其产生原因和掌握正确的处理方法对于编写健壮的 Java 代码非常重要。在遍历集合时,应尽量避免在遍历过程中修改集合结构;如果需要修改,可使用迭代器的 remove() 方法、Copy - on - Write 集合或并发集合类来处理。

参考资料