深入理解 Java 中的 ConcurrentModificationException 及其解决方案

发布于:2025-02-10 ⋅ 阅读:(58) ⋅ 点赞:(0)

目录

前言

一、ConcurrentModificationException 的基本概念

1. 定义

2. 什么是结构性修改?

二、ConcurrentModificationException 的触发场景

1. 单线程环境中的触发

2. 多线程环境中的触发

三、ConcurrentModificationException 的底层原理

四、解决 ConcurrentModificationException 的方法

方法 1:使用 Iterator 的 remove() 方法

方法 2:使用 CopyOnWriteArrayList

方法 3:使用普通的 for 循环

方法 4:使用 Synchronized 同步块

五、总结

前言

在 Java 的日常开发中,ConcurrentModificationException (并发修改异常)是一个常见的异常,尤其是在操作集合(如 ListMap 等)时。如果对集合进行迭代的同时又修改了集合,就有可能抛出这个异常。本文将对 ConcurrentModificationException 进行深入剖析,了解其产生的原因,以及解决该异常的多种方式。


一、ConcurrentModificationException 的基本概念

1. 定义

ConcurrentModificationException 是 Java 中的运行时异常,位于包 java.util 下。它表示当一个线程对集合进行迭代的过程中,同时检测到另一个线程或同一线程对该集合进行了结构性修改时抛出的异常。

2. 什么是结构性修改?

结构性修改是指改变集合的结构,例如:

  • 添加元素
  • 删除元素
  • 清空集合

非结构性修改(如通过 set() 方法替换列表中的某个值)通常不会触发此异常,因为它不会改变集合的结构。


二、ConcurrentModificationException 的触发场景

1. 单线程环境中的触发

即使在单线程环境中,如果在迭代集合时直接对集合进行增删操作,也会抛出该异常。例如:

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

public class SingleThreadExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        for (String item : list) {
            if ("A".equals(item)) {
                list.remove(item); // 在迭代时修改集合
            }
        }
    }
}


//输出
Exception in thread "main" java.util.ConcurrentModificationException

原因for-each 循环底层使用了 Iterator。当通过 Iterator 遍历集合时,直接对集合进行修改会触发 modCount 不一致,从而抛出异常。

2. 多线程环境中的触发

在多线程环境下,一个线程正在遍历集合,而另一个线程对集合进行修改,也可能抛出该异常。例如:

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

public class MultiThreadExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        new Thread(() -> {
            for (String item : list) {
                System.out.println(item);
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(20);
                list.add("D"); // 修改集合
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

输出:可能会抛出 ConcurrentModificationException

原因:两个线程同时操作集合,导致迭代器检测到集合的 modCount 被改变。


三、ConcurrentModificationException 的底层原理

在 Java 集合(如 ArrayListHashMap)中,迭代器会维护一个 modCount 变量来跟踪集合的修改次数。当迭代器创建时,它会保存当前的 modCount 值。在迭代过程中,每次调用 next() 方法时,迭代器会检查集合当前的 modCount 是否与初始值一致。如果不一致,就说明集合被修改过,于是抛出 ConcurrentModificationException

modCount 的核心代码示例如下(以 ArrayList 为例):

public class ArrayList<E> extends AbstractList<E> {
    protected transient int modCount = 0; // 修改计数

    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }

    public E remove(int index) {
        modCount++; // 每次结构性修改都会增加 modCount
        E oldValue = elementData(index);
        fastRemove(index);
        return oldValue;
    }
}

Iterator 中的检查逻辑:

public E next() {
    if (modCount != expectedModCount) // 检查是否有不一致
        throw new ConcurrentModificationException();
    return (E) ArrayList.this.elementData(cursor++);
}

四、解决 ConcurrentModificationException 的方法

方法 1:使用 Iterator 的 remove() 方法

如果需要在迭代时修改集合,可以使用 Iterator 提供的 remove() 方法,而不是直接调用集合的 remove() 方法。

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

public class IteratorRemoveExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
            if ("A".equals(item)) {
                iterator.remove(); // 正确的删除方式
            }
        }

        System.out.println(list); // 输出:[B, C]
    }
}
方法 2:使用 CopyOnWriteArrayList

在多线程环境中,使用 CopyOnWriteArrayList 替代普通的 ArrayListCopyOnWriteArrayList 是线程安全的,它在每次修改时都会创建集合的副本,从而避免 ConcurrentModificationException

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

public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        for (String item : list) {
            if ("A".equals(item)) {
                list.remove(item); // 不会抛出 ConcurrentModificationException
            }
        }

        System.out.println(list); // 输出:[B, C]
    }
}

注意CopyOnWriteArrayList 在修改性能上较差,适用于读多写少的场景。


方法 3:使用普通的 for 循环

普通的 for 循环可以直接操作集合,不会触发迭代器的 modCount 检查。

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

public class ForLoopExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        for (int i = 0; i < list.size(); i++) {
            if ("A".equals(list.get(i))) {
                list.remove(i); // 直接删除
                i--; // 删除后需要调整索引
            }
        }

        System.out.println(list); // 输出:[B, C]
    }
}
方法 4:使用 Synchronized 同步块

在多线程环境中,可以使用同步块对集合进行显式加锁,从而避免其他线程同时修改集合。

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

public class SynchronizedExample {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        list.add("A");
        list.add("B");
        list.add("C");

        synchronized (list) { // 显式加锁
            for (String item : list) {
                if ("A".equals(item)) {
                    list.remove(item);
                }
            }
        }

        System.out.println(list); // 输出:[B, C]
    }
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SynchronizedExample {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        list.add("A");
        list.add("B");
        list.add("C");

        synchronized (list) { // 显式加锁
            for (String item : list) {
                if ("A".equals(item)) {
                    list.remove(item);
                }
            }
        }

        System.out.println(list); // 输出:[B, C]
    }
}

五、总结

ConcurrentModificationException (并发修改异常) 的出现通常是因为在迭代集合时同时修改了集合。要避免该异常,应该根据具体场景选择合适的解决方案:

  • 在单线程环境下,使用迭代器的 remove() 方法。
  • 在多线程环境下,使用线程安全的集合如 CopyOnWriteArrayList 或对集合加锁。
  • 对于简单场景,可以使用普通的 for 循环。

        通过合理的方式处理集合的修改,不仅能避免异常,还能提升程序的稳定性和可维护性。希望本文能帮助您深入理解 ConcurrentModificationException 及其解决方案。如有任何疑问,欢迎交流!


网站公告

今日签到

点亮在社区的每一天
去签到