为什么会引发 ConcurrentModificationException 以及如何调试它

Why is a ConcurrentModificationException thrown and how to debug it

提问人:mainstringargs 提问时间:3/2/2009 最后编辑:Raedwaldmainstringargs 更新时间:1/16/2023 访问量:217525

问:

我正在使用 a(JPA 间接使用,它碰巧如此),但显然代码随机抛出一个 .是什么原因导致的,我该如何解决这个问题?也许通过使用一些同步?CollectionHashMapConcurrentModificationException

以下是完整的堆栈跟踪:

Exception in thread "pool-1-thread-1" java.util.ConcurrentModificationException
        at java.util.HashMap$HashIterator.nextEntry(Unknown Source)
        at java.util.HashMap$ValueIterator.next(Unknown Source)
        at org.hibernate.collection.AbstractPersistentCollection$IteratorProxy.next(AbstractPersistentCollection.java:555)
        at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:296)
        at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:242)
        at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:219)
        at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:169)
        at org.hibernate.engine.Cascade.cascade(Cascade.java:130)
Java 异常 集合 ConcurrentModification

评论

1赞 ordnungswidrig 3/3/2009
你能提供更多的背景信息吗?您是否正在合并、更新或删除实体?这个实体有什么关联?您的级联设置如何?
1赞 Chochos 8/31/2010
从堆栈跟踪中,您可以看到在遍历 HashMap 时发生了异常。当然,其他线程正在修改地图,但异常发生在正在迭代的线程中。

答:

2赞 duffymo 3/2/2009 #1

这听起来不像是 Java 同步问题,而更像是数据库锁定问题。

我不知道向所有持久类添加一个版本是否会解决这个问题,但这是 Hibernate 可以提供对表中行的独占访问的一种方式。

可能是隔离级别需要更高。如果你允许“脏读”,也许你需要提高到可序列化。

评论

0赞 duffymo 10/9/2014
我认为他们的意思是 Hashtable。它作为 JDK 1.0 的一部分提供。像 Vector 一样,它被写成线程安全 - 而且速度很慢。两者都已被非线程安全的替代品所取代:HashMap 和 ArrayList。按实际使用量付费。
308赞 Robin 3/2/2009 #2

这不是同步问题。如果迭代器本身以外的任何内容都修改了正在迭代的基础集合,则会发生这种情况。

Iterator it = map.entrySet().iterator();
while (it.hasNext()) {
    Entry item = it.next();
    map.remove(item.getKey());
}

这将在第二次调用时抛出一个。ConcurrentModificationExceptionit.hasNext()

正确的方法是

Iterator it = map.entrySet().iterator();
while (it.hasNext()) {
    Entry item = it.next();
    it.remove();
}

假设此迭代器支持该操作。remove()

评论

1赞 Tom Hawtin - tackline 3/2/2009
可能,但看起来 Hibernate 正在进行迭代,这应该合理正确地实现。可能会有回调修改地图,但这不太可能。这种不可预测性指向了实际的并发性问题。
0赞 Robin 3/2/2009
此异常与线程并发无关,它是由被修改的迭代器的后备存储引起的。是否通过另一个线程对迭代器来说无关紧要。恕我直言,这是一个名称不佳的例外,因为它给人一种不正确的原因印象。
0赞 Robin 3/2/2009
但是,我同意,如果不可预测,则很可能是线程问题导致发生此异常的条件。由于异常名称,这使得它更加令人困惑。
0赞 G__ 5/10/2011
这是正确的,并且比公认的答案更好的解释,但公认的答案是一个很好的解决方案。ConcurrentHashMap 不受 CME 的约束,即使在迭代器中也是如此(尽管迭代器仍设计用于单线程访问)。
0赞 peter 8/21/2012
这个解决方案没有意义,因为 Maps 没有 iterator() 方法。Robin 的例子适用于例如列表。
0赞 Javamann 3/3/2009 #3

尝试 CopyOnWriteArrayList 或 CopyOnWriteArraySet,具体取决于您尝试执行的操作。

90赞 Chochos 3/3/2009 #4

尝试使用 a 而不是普通ConcurrentHashMapHashMap

评论

0赞 tobiasbayer 8/31/2010
这真的解决了问题吗?我遇到了同样的问题,但我肯定可以排除任何线程问题。
10赞 Chochos 8/31/2010
另一种解决方案是创建地图的副本并循环访问该副本。或者复制一组键并循环访问它们,从原始映射中获取每个键的值。
1赞 Valchris 3/16/2011
即时救世主。去研究为什么这运作得这么好,这样我就不会在以后得到更多的惊喜。
1赞 Rais Alam 12/24/2012
我想它不是同步问题,如果在循环同一对象时修改相同的修改,那就是问题。
2赞 juanmf 12/15/2016
来自 ConcurrentHashMap JavadocSimilarly, Iterators, Spliterators and Enumerations return elements reflecting the state of the hash table at some point at or since the creation of the iterator/enumeration. They do not throw ConcurrentModificationException. However, iterators are designed to be used by only one thread at a time.
1赞 ZhaoGang 7/6/2018 #5

请注意,如果您像我一样在迭代地图时尝试从地图中删除某些条目,则在进行某些修改之前,所选答案不能直接应用于您的上下文。

我只是在这里为新手提供我的工作示例,以节省他们的时间:

HashMap<Character,Integer> map=new HashMap();
//adding some entries to the map
...
int threshold;
//initialize the threshold
...
Iterator it=map.entrySet().iterator();
while(it.hasNext()){
    Map.Entry<Character,Integer> item=(Map.Entry<Character,Integer>)it.next();
    //it.remove() will delete the item from the map
    if((Integer)item.getValue()<threshold){
        it.remove();
    }

评论

1赞 Nafiul Alam Fuji 7/23/2022
神奇的是“it.remove()”而不是“map.remove(item.getKey())”。就是这样
34赞 Raedwald 3/13/2019 #6

大多数类都不允许在使用迭代器循环访问集合时修改集合。Java 库将尝试修改 a 时迭代它称为“并发修改”。不幸的是,这表明唯一可能的原因是多个线程同时修改,但事实并非如此。仅使用一个线程,就可以为 (使用 Collection.iterator() 或增强的 for 循环)创建一个迭代器),开始迭代(使用 Iterator.next(),或等效地输入增强循环的主体),修改 ,然后继续迭代。CollectionCollectionCollectionCollectionforCollection

为了帮助程序员,这些类的一些实现会尝试检测错误的并发修改,如果检测到它,则抛出一个。但是,通常不可能保证检测到所有并发修改。因此,错误地使用 并不总是导致抛出 .CollectionConcurrentModificationExceptionCollectionConcurrentModificationException

ConcurrentModificationException 的文档说:

当不允许同时修改对象时,检测到对象并发修改的方法可能会引发此异常...

请注意,此异常并不总是指示对象已被其他线程并发修改。如果单个线程发出一系列违反对象约定的方法调用,则该对象可能会抛出此异常...

请注意,不能保证快速故障行为,因为一般来说,在存在不同步并发修改的情况下,不可能做出任何硬性保证。快速失败的操作是在尽最大努力的基础上进行的。ConcurrentModificationException

请注意,

HashSetHashMapTreeSetArrayList 类的文档是这样说的:

[直接或间接从此类]返回的迭代器是快速失败的:如果在创建迭代器后的任何时间修改了 [collection],则除了通过迭代器自己的 remove 方法之外,还会抛出 .因此,在面对并发修改时,迭代器会快速而干净地失败,而不是冒着在未来不确定时间出现任意、非确定性行为的风险。IteratorConcurrentModificationException

请注意,迭代器的快速失效行为无法得到保证,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬性保证。快速失败的迭代器在尽最大努力的基础上抛出。因此,编写一个依赖于此异常的程序的正确性是错误的:迭代器的快速故障行为应该只用于检测错误ConcurrentModificationException

再次注意,该行为“不能保证”,只是“在尽力而为的基础上”。

Map 接口的几种方法的文档是这样说的:

非并发实现应重写此方法,如果检测到映射函数在计算过程中修改了此映射,则应尽最大努力抛出 a。并发实现应重写此方法,如果检测到映射函数在计算过程中修改了此映射,则应尽最大努力抛出一个,因此计算将永远不会完成。ConcurrentModificationExceptionIllegalStateException

再次注意,检测只需要“尽力而为”,并且仅对非并发(非线程安全)类明确建议使用。ConcurrentModificationException

调试ConcurrentModificationException

因此,当您看到由于 而导致的堆栈跟踪时,您不能立即假设原因是对 .您必须检查堆栈跟踪以确定哪个类引发了异常(该类的方法将直接或间接引发异常)以及针对哪个对象。然后,您必须检查可以从何处修改该对象。ConcurrentModificationExceptionCollectionCollectionCollection

  • 最常见的原因是修改了 内 增强的循环。仅仅因为您在源代码中没有看到对象并不意味着那里没有对象!幸运的是,故障循环的语句之一通常位于堆栈跟踪中,因此跟踪错误通常很容易。CollectionforCollectionIteratorIteratorfor
  • 更棘手的情况是,代码传递对对象的引用。请注意,集合的不可修改视图(例如由 Collections.unmodifiableList() 生成)保留了对可修改集合的引用,因此对“不可修改”集合的迭代可能会引发异常(修改已在其他地方完成)。您的其他视图(如子列表、地图条目集和地图关键集)也保留对原始视图(可修改)的引用。 即使对于线程安全,例如 CopyOnWriteList,这也可能是一个问题;不要假定线程安全(并发)集合永远不会引发异常。CollectionCollectionCollectionCollection
  • 在某些情况下,哪些操作可以修改 a 可能是意想不到的。例如,LinkedHashMap.get() 修改其集合Collection
  • 最困难的情况是当异常由于多个线程的并发修改造成的。

编程以防止并发修改错误

如果可能,请将所有引用限制在对象上,这样更容易防止并发修改。使对象或局部变量成为对象,并且不要从方法返回对 或其迭代器的引用。然后,检查所有可以修改的地方要容易得多。如果要由多个线程使用,那么确保线程仅通过适当的同步和锁定访问线程是可行的。CollectionCollectionprivateCollectionCollectionCollectionCollection

评论

2赞 MasterJoe 7/18/2020
我想知道为什么在单个线程的情况下不允许并发修改。如果允许单个线程在常规哈希映射上进行并发修改,会发生什么问题?
16赞 Zentopia 11/20/2019 #7

在 Java 8 中,您可以使用 lambda 表达式:

map.keySet().removeIf(key -> key condition);

removeIf是一种方便的方法,在内部使用 来循环访问调用集合的元素。 删除条件的提取通过允许调用方提供 .defaultCollectionIteratorPredicate<? super E>

“我将为您执行迭代,并测试集合中的每个元素。如果一个元素导致 the 的方法返回,我将删除它。PredicatetestPredicatetrue

评论

0赞 PlsWork 1/13/2021
美丽。这应该放在顶部。是 O(n) 时间,是 O(1) 空间?
0赞 S_K 6/16/2021
这是更优雅的解决方案。谢谢。
0赞 grayman 6/15/2020 #8

当我尝试从列表中删除最后 x 项时,我遇到了此异常。 是唯一对我有用的解决方案。myList.subList(lastIndex, myList.size()).clear();