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

问题描述 投票:109回答:6

我正在使用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)
java collections concurrentmodification
6个回答
241
投票

这不是同步问题。如果正在迭代的基础集合被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()操作。


58
投票

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


8
投票

大多数Collection类都不允许在使用Collection迭代Iterator时修改Collection。 Java库调用尝试修改Collection,同时在其中迭代“并发修改”,遗憾的是,建议唯一可能的原因是多个线程同时修改,但事实并非如此。只使用一个线程就可以为Collection创建一个迭代器(使用Collection.iterator()enhanced for loop),开始迭代(使用Iterator.next(),或者等效地输入增强的for循环体),修改Collection,然后继续迭代。

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

ConcurrentModificationException的文档说:

当不允许这样的修改时,检测到对象的并发修改的方法可能抛出此异常...

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

请注意,无法保证快速失败的行为,因为一般来说,在存在不同步的并发修改时,不可能做出任何硬性保证。快速失败的操作会尽最大努力抛出ConcurrentModificationException

注意

HashSetHashMapTreeSetArrayList类的文档说明了这一点:

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

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

再次注意,行为“无法保证”,并且只是“尽力而为”。

Map界面的几种方法的文档说:

非并发实现应该重写此方法,并且如果检测到映射函数在计算期间修改此映射,则在尽力而为的基础上抛出ConcurrentModificationException。并发实现应该覆盖此方法,并且在尽力而为的基础上,如果检测到映射函数在计算期间修改此映射并且因此计算将永远不会完成,则抛出IllegalStateException

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

Debugging ConcurrentModificationException

因此,当您看到由于ConcurrentModificationException导致的堆栈跟踪时,您无法立即认为原因是对Collection的不安全的多线程访问。你必须examine the stack-trace来确定哪一类Collection抛出异常(类的方法将直接或间接抛出它),以及Collection对象。然后,您必须检查可以修改该对象的位置。

  • 最常见的原因是在Collection上增强的for环内修改Collection。只是因为你没有在源代码中看到Iterator对象并不意味着那里没有Iterator!幸运的是,错误的for循环的一个语句通常会在堆栈跟踪中,因此跟踪错误通常很容易。
  • 更棘手的情况是你的代码传递对Collection对象的引用。请注意,集合的不可修改视图(例如由Collections.unmodifiableList()生成)保留对可修改集合的引用,因此iteration over an "unmodifiable" collection can throw the exception(修改已在其他地方完成)。你的Collection的其他观点,如sub listsMap entry setsMap key sets也保留对原始(可修改的)Collection的引用。即使对于线程安全的Collection,例如CopyOnWriteList,这也是一个问题;不要假设线程安全(并发)的collectins永远不会抛出异常。
  • 在某些情况下,哪些操作可以修改Collection可能是意料之外的。例如,LinkedHashMap.get() modifies its collection
  • 最困难的情况是异常是由多个线程同时修改引起的。

Programming to prevent concurrent modification errors

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


2
投票

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

我不知道在所有持久化类中添加一个版本是否会将其排序,但这是Hibernate可以提供对表中行的独占访问的一种方式。

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


0
投票

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


0
投票

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

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

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();
    }
© www.soinside.com 2019 - 2024. All rights reserved.