为什么抛出 ConcurrentModficationException 以及如何调试它

我正在使用一个 Collection(一个由 JPA 间接使用的 HashMap,这种情况时有发生) ,但显然代码随机抛出一个 ConcurrentModificationException。是什么导致了这个问题? 我该如何解决这个问题?也许通过一些同步?

下面是完整的堆栈跟踪:

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)
198194 次浏览

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

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

可能需要更高的隔离级别。如果允许“脏读”,那么可能需要升级为可序列化。

这不是同步问题。如果正在迭代的基础集合被 Iterator 本身以外的任何东西修改,就会发生这种情况。

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

这将在第二次调用 it.hasNext()时抛出 ConcurrentModificationException

正确的方法是

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

假设这个迭代器支持 remove()操作。

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

尝试使用 ConcurrentHashMap而不是普通的 HashMap

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

我只是给新手们举个例子来节省他们的时间:

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();
}

大多数 Collection类在使用 Collection2迭代该 Collection时修改 Collection1是 Collection3。Java 库调用尝试修改 Collection,同时在其中迭代“并发修改”。不幸的是,这表明唯一可能的原因是多个线程同时修改,但事实并非如此。只使用一个线程就可以为 Collection创建一个迭代器(使用 Collection4或者 Collection5) ,开始迭代(使用 Collection6,或者等效地进入增强的 for循环体) ,修改 Collection,然后继续迭代。

为了帮助程序员,这些 Collection尝试一些实现可以检测错误的并发修改,并在检测到时抛出 ConcurrentModificationException。然而,一般来说,保证检测所有并发修改是不可能和不实际的。因此,错误地使用 Collection并不总是导致抛出 ConcurrentModificationException

ConcurrentModificationException的文件显示:

此异常可能由检测到对象的并发修改的方法引发,当这种修改是不允许的..。

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

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

请注意

HashSetHashMapTreeSetArrayList类的文档说明如下:

返回的迭代器[直接或间接从这个类]是快速失败的: 如果在创建迭代器之后的任何时候修改了[集合] ,除了通过迭代器自己的 delete 方法之外,Iterator抛出一个 ConcurrentModificationException。因此,在面对并发修改时,迭代器会迅速而干净地失败,而不会在未来某个不确定的时间冒任意、非确定性行为的风险。

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

请再次注意,这种行为“无法保证”,而且只是“在尽最大努力的基础上”。

Map接口的几个方法的文档说明如下:

非并发实现应该覆盖此方法,并且如果检测到映射函数在计算期间修改了此映射,则应尽最大努力抛出 ConcurrentModificationException。并发实现应该覆盖这个方法,如果检测到映射函数在计算期间修改了这个映射,并且因此计算永远不会完成,那么应该尽最大努力抛出 IllegalStateException

再次注意,检测只需要“尽最大努力”,并且只对非并发(非线程安全)类显式建议使用 ConcurrentModificationException

调试 ConcurrentModificationException

因此,当您看到由于 ConcurrentModificationException导致的堆栈跟踪时,不能立即假定原因是对 Collection的不安全多线程访问。您必须使用 检查堆栈跟踪来确定哪个 Collection类引发了异常(该类的一个方法将直接或间接引发异常) ,以及哪个 Collection对象。然后,您必须检查从何处可以修改该对象。

  • 最常见的原因是在 Collection上增强的 for循环中修改了 Collection。仅仅因为在源代码中没有看到 Iterator对象并不意味着那里没有 Iterator!幸运的是,故障 for循环的一个语句通常位于堆栈跟踪中,因此跟踪错误通常很容易。
  • 更棘手的情况是当您的代码传递对 Collection对象的引用时。请注意,集合的 无法改变视图(如由 Collections.unmodifiableList()生成的)保留了对可修改集合的引用,因此 Collections.unmodifiableList()0(修改已在其他地方完成)。你的 Collection的其他 Collections.unmodifiableList()1,如 Collections.unmodifiableList()2,Collections.unmodifiableList()3和 Collections.unmodifiableList()4也保留了对原始(可修改的) Collection的引用。即使对于线程安全的 Collection(例如 Collections.unmodifiableList()5) ,这也可能是一个问题; 不要假设线程安全(并发)集合永远不会抛出异常。
  • 在某些情况下,哪些操作可以修改 Collection是意想不到的。
  • 最困难的情况是由于多个线程的并发修改而导致异常

防止并发修改错误的编程

在可能的情况下,将所有引用限制在一个 Collection对象上,这样可以更容易地防止并发修改。使 Collection成为 private对象或局部变量,并且不从方法返回对 Collection或其迭代器的引用。这样就更容易检查 所有中可以修改 Collection的位置。如果 Collection将由多个线程使用,那么可以确保这些线程只通过适当的同步和锁定来访问 Collection

在 Java8中,可以使用 lambda 表达式:

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

我在试图从列表中删除 x 最后一项时遇到了这个异常。 myList.subList(lastIndex, myList.size()).clear();是唯一适合我的解决方案。