concurrentmodificationexception(cme)的解决方法有四种:1. 使用迭代器的remove()方法;2. 使用并发集合类;3. 在迭代前创建集合的副本;4. 使用同步机制。cme通常在遍历集合时修改其结构导致,即使在单线程中也会发生,这是java的“快速失败”机制通过modcount变量检测到集合被外部修改的结果。使用迭代器自身的remove()方法可避免cme,因其内部做了特殊处理;并发集合如copyonwritearraylist、concurrenthashmap等通过写入复制或分段锁机制避免异常,并提供高并发性能;若不需实时修改,可在遍历副本后修改原始集合;对非并发集合则需手动加锁以确保同步。copyonwrite机制虽能避免cme,但会带来内存开销和弱一致性问题,适合读多写少的场景。其他并发集合如concurrentlinkedqueue和blockingqueue实现也各具优势,适用于不同并发需求。

ConcurrentModificationException (CME) 多数情况下,是因为你在遍历一个集合的同时,又试图修改这个集合的结构(比如添加、删除元素),而且这种修改没有通过迭代器自身提供的方法来完成。解决这个问题的核心思路,要么是确保修改和遍历的原子性,要么是使用专门为并发场景设计的集合类,要么就是巧妙地避开这种同时操作。

要避免ConcurrentModificationException,我们有几种行之有效的方法,每种都有其适用场景和考量:

使用迭代器(Iterator)的 remove() 方法:
这是最直接也最推荐的方式,当你需要在遍历过程中删除元素时。Iterator接口提供了一个remove()方法,这个方法在集合内部做了特殊处理,确保在删除元素时不会触发CME。但要注意,这个方法只能删除当前迭代器指向的元素。如果你在for-each循环中尝试修改集合,或者通过集合自身的add()/remove()方法在迭代时修改,CME就会不期而至。
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if ("B".equals(element)) {
iterator.remove(); // 正确的删除方式
}
}
System.out.println(list); // 输出: [A, C, D]使用并发集合类(Concurrent Collections):
Java的java.util.concurrent包提供了许多线程安全的集合类,它们在设计上就考虑了并发修改的问题,并且通常不会抛出CME。

CopyOnWriteArrayList / CopyOnWriteArraySet:
这类集合在每次修改操作时,都会创建一个底层数组的新副本。迭代器遍历的是旧的副本,因此不会受到修改的影响。这非常适合读操作远多于写操作的场景。缺点是写操作开销大,且迭代器看到的是修改前的数据快照(“弱一致性”)。ConcurrentHashMap:
这是一个高度并发的哈希表,通过分段锁(Java 7及以前)或CAS操作(Java 8及以后)实现了极高的并发性能,而不会抛出CME。ConcurrentLinkedQueue / ConcurrentLinkedDeque:
非阻塞的并发队列,适用于生产者-消费者模型。BlockingQueue 的实现类(如ArrayBlockingQueue, LinkedBlockingQueue):
这些队列在存取元素时会阻塞,是处理并发任务的强大工具。在迭代前创建集合的副本: 如果你不需要在迭代过程中修改原始集合,或者修改量不大,可以先将集合复制一份,然后遍历副本,对原始集合进行修改。这种方式简单粗暴,但有效。
List<String> originalList = new ArrayList<>(Arrays.asList("X", "Y", "Z"));
List<String> copyList = new ArrayList<>(originalList); // 创建副本
for (String item : copyList) {
if ("Y".equals(item)) {
originalList.remove(item); // 修改原始集合
}
}
System.out.println(originalList); // 输出: [X, Z]使用同步机制(Synchronization):
如果你必须使用非并发集合(如ArrayList或HashMap),并且在多线程环境下进行操作,那么你需要手动进行同步。可以使用synchronized关键字,或者java.util.concurrent.locks包下的锁。
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 在遍历时,也必须对集合进行同步
synchronized (syncList) {
for (String item : syncList) {
// ...
}
}
// 修改时也需要同步
synchronized (syncList) {
syncList.add("new item");
}这种方式虽然能避免CME,但会降低并发性,因为在同步块内,只有一个线程能访问集合。
说实话,ConcurrentModificationException这个名字,很容易让人误解它只发生在多线程并发修改的场景。但实际上,它更多的是一种“快速失败(fail-fast)”机制的体现,即使在单线程环境下,如果操作不当,它也可能跳出来给你一个“惊喜”。
想象一下,你正在翻阅一本书(集合),突然有人在你翻阅的时候,从中间撕掉了一页(修改了集合结构),或者在某个地方插入了一页。你手里拿着的目录(迭代器)瞬间就失效了,因为它不再能准确地指向书页了。这就是CME的本质。
Java的大多数非线程安全集合类(如ArrayList, HashMap, HashSet等)的迭代器都实现了这种“快速失败”机制。它们内部通常维护一个modCount(modification count)变量。每当集合的结构被修改时(比如add, remove, clear等操作),这个modCount就会增加。当迭代器在进行next()或hasNext()操作时,它会检查当前集合的modCount是否与它创建时记录的modCount一致。如果不一致,就意味着在迭代过程中集合被外部修改了,迭代器就会立即抛出ConcurrentModificationException,而不是继续在一个不确定的状态下操作,从而避免了潜在的逻辑错误和数据不一致。
举个例子,你用一个for-each循环遍历一个ArrayList:
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
for (Integer number : numbers) {
if (number == 3) {
numbers.remove(number); // 这里会抛出 ConcurrentModificationException
}
}这里的for-each循环实际上是Java语法糖,它在底层隐式地使用了迭代器。当你调用numbers.remove(number)时,ArrayList的modCount增加了,但迭代器并不知道这个变化,在下一次next()调用时,它发现modCount不匹配,于是就抛出了CME。这就是为什么我一直强调,如果你要在迭代过程中修改集合,一定要使用迭代器自身的remove()方法。
CopyOnWrite机制,比如CopyOnWriteArrayList和CopyOnWriteArraySet,确实是解决并发修改导致CME的强大工具。它的核心思想正如其名:写入时复制(Copy On Write)。
当你对CopyOnWriteArrayList进行修改操作(add, set, remove等)时,它不会直接在原有的底层数组上修改,而是会先复制一份原数组,然后在新的副本上进行修改,最后再将集合的内部引用指向这个新的副本。而读操作(包括迭代)则始终操作的是旧的、未修改的数组。
优点:
ConcurrentModificationException。缺点和限制:
ConcurrentHashMap等更精细粒度锁的并发集合。所以,CopyOnWrite并非万能药。它解决了CME,提供了高并发读,但牺牲了内存和写性能,并且引入了弱一致性。在选择它之前,你真的需要权衡你的具体应用场景:是读多写少吗?对内存消耗敏感吗?能否接受迭代器看到旧数据?如果这些答案都是肯定的,那么CopyOnWrite就是你的利器。如果不是,你可能需要考虑其他并发集合或者更细粒度的同步机制。
Java的java.util.concurrent包简直是并发编程的宝库,里面有很多集合类都能有效避免ConcurrentModificationException,并且各有侧重。它们通常通过更精妙的锁机制(比如分段锁、CAS操作)或者无锁算法来实现线程安全。
ConcurrentHashMap:
这是我个人最常用,也是我认为最强大的并发Map实现。它不是简单地给整个Map加锁,而是将Map分成多个“段”(在Java 7及以前),或者在Java 8中通过更细粒度的CAS(Compare-And-Swap)操作和Node数组+链表/红黑树的组合来保证并发安全。这意味着多个线程可以同时对Map的不同部分进行读写操作,而不会相互阻塞,从而提供了极高的并发性能。它不会抛出CME,并且迭代器提供的是“弱一致性”视图,即迭代器在遍历时可能不会反映出Map在迭代过程中发生的最新修改,但它能保证不会抛出CME。
ConcurrentLinkedQueue 和 ConcurrentLinkedDeque:
它们是基于链表实现的无界非阻塞队列。这意味着在入队(offer)和出队(poll)操作时,它们不会阻塞线程,而是通过CAS操作来保证线程安全。它们非常适合生产者-消费者模型,或者任何需要高并发、非阻塞队列的场景。同样,它们在迭代时也不会抛出CME,迭代器也提供弱一致性。
BlockingQueue 的各种实现:
BlockingQueue是一个接口,它提供了在队列为空时获取元素阻塞,以及在队列满时添加元素阻塞的功能。它的实现类有很多,比如:
ArrayBlockingQueue:基于数组的有界阻塞队列,内部使用ReentrantLock实现同步。LinkedBlockingQueue:基于链表的有界(默认无界)阻塞队列,内部使用两把锁(一把用于入队,一把用于出队)实现,性能通常比ArrayBlockingQueue好。PriorityBlockingQueue:支持优先级的无界阻塞队列。DelayQueue:支持延迟获取元素的无界阻塞队列。SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待一个对应的移除操作,反之亦然。
这些BlockingQueue的实现都天然地是线程安全的,并且它们的设计目的就是为了解决并发环境下的数据传递和同步问题,因此自然不会出现CME。它们是构建并发流程和解耦生产者-消费者逻辑的基石。选择哪种并发集合,取决于你的具体需求:是需要Map?Queue?Set?对性能和一致性的要求是什么?读写频率如何?了解它们的内部机制和适用场景,才能做出最合适的选择。
是的,ConcurrentModificationException 完全有可能在单线程环境中发生。这是一个常见的误解,认为这个异常只与多线程并发有关。实际上,它的核心在于“迭代器在遍历集合时,集合的结构被非迭代器方法修改了”。
让我来解释一下。我们之前提到了“快速失败”机制,即迭代器会检查modCount。这个modCount的检查,与有多少个线程在操作无关,只与集合的结构是否在迭代器创建后被外部修改有关。
考虑以下单线程代码片段:
List<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Orange"));
// 场景一:使用 for-each 循环,并在循环体内通过集合自身方法修改
for (String fruit : fruits) {
if ("Banana".equals(fruit)) {
fruits.remove(fruit); // 这里会抛出 ConcurrentModificationException
}
}
System.out.println(fruits);在这个例子中,for-each循环在底层使用了迭代器。当fruits.remove(fruit)被调用时,fruits集合的modCount增加了。当循环进入下一次迭代,迭代器在执行next()操作时,它发现当前的modCount与它初始化时记录的不一致,于是立即抛出ConcurrentModificationException。整个过程都在一个线程内完成。
再看一个例子,即使你不用for-each,而是传统的for循环,如果你在循环中通过集合的remove()方法修改了集合的大小,也可能导致逻辑错误(虽然不一定是CME,但可能跳过元素或索引越界)。
所以,无论是在单线程还是多线程环境下,避免CME的关键在于:当你在遍历一个集合时,如果需要修改它的结构,请务必使用迭代器(Iterator)自身提供的remove()方法。如果你通过集合自身的add()、remove()、clear()等方法来修改,那么CME就可能随时出现。这个异常的出现,其实是Java设计者的一种善意提醒:你的代码可能存在逻辑漏洞,或者你没有正确地处理集合的并发或迭代修改。它强制你面对并解决潜在的问题,而不是让程序在不确定的状态下继续运行。
以上就是如何避免ConcurrentModificationException?的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号