为什么会引发ConcurrentModificationException以及如何对其进行调试


130

我正在使用CollectionHashMapJPA间接使用的a ,它确实发生了),但显然代码是随机抛出的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)

1
您能否提供更多背景信息?您要合并,更新或删除实体吗?这个实体没有什么关联?您的级联设置如何?
ordnungswidrig,2009年

1
从堆栈跟踪中,您可以看到在遍历HashMap时发生了异常。当然,其他线程正在修改该映射,但是例外发生在正在迭代的线程中。
Chochos'Aug

Answers:


263

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

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
可能会出现,但似乎Hibernate正在执行迭代,应该合理正确地实现它。可能会有回调修改地图,但这不太可能。不可预测性指向实际的并发问题。
汤姆·哈顿

此异常与线程并发无关,它是由修改迭代器的后备存储引起的。迭代器是否通过另一个线程无关紧要。恕我直言,这是一个名称不正确的异常,因为它给人以错误的印象。
罗宾

但是,我同意,如果这是不可预测的,则很可能会出现线程问题,从而导致发生此异常的条件。由于异常名称,这使它更加混乱。
罗宾(Robin)2009年

这是正确的并且比接受的答案更好的解释,但是接受的答案是很好的解决方案。即使在迭代器内部,ConcurrentHashMap也不受CME的限制(尽管该迭代器仍设计用于单线程访问)。
G__

该解决方案没有意义,因为Maps没有iterator()方法。罗宾的例子将适用于例如列表。
彼得2012年

72

尝试使用A ConcurrentHashMap代替普通HashMap


那真的解决了问题吗?我遇到同样的问题,但是我可以肯定地排除所有线程问题。
tobiasbayer

5
另一个解决方案是创建地图的副本,然后遍历该副本。或复制一组键并进行遍历,以从原始映射中获取每个键的值。
2010年

Hibernate在整个集合中进行迭代,因此您不能简单地复制它。
tobiasbayer's

1
即时救星。要研究为什么它如此有效,所以在以后的工作中我不会再有更多的惊喜。
2011年

1
我猜它不是同步问题,如果在循环相同对象时进行相同修改的修改是有问题的。
Rais Alam

17

的变形Collection通过,尽管迭代Collection使用Iterator不允许被大多数的Collection类。Java库将Collection在迭代过程中进行修改的尝试称为“并行修改”。不幸的是,这表明唯一可能的原因是多个线程同时进行了修改,但事实并非如此。仅使用一个线程就可以为Collection(使用Collection.iterator()增强的for循环)创建一个迭代器,开始进行迭代(使用Iterator.next()或等效地进入增强的for循环的主体),修改Collection,然后继续进行迭代。

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

的文档ConcurrentModificationException说:

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

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

请注意,不能保证快速故障行为,因为通常来说,在存在不同步的并发修改的情况下,不可能做出任何严格的保证。快速失败的操作ConcurrentModificationException是尽力而为的。

注意

的文档HashSetHashMapTreeSetArrayList类这样说:

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

注意,迭代器的快速失败行为无法得到保证,因为通常来说,在存在不同步的并发修改的情况下,不可能做出任何严格的保证。快速失败的迭代器会ConcurrentModificationException尽力而为。因此,编写依赖于此异常的程序的正确性是错误的:迭代器的快速失败行为应仅用于检测错误

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

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

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

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

调试 ConcurrentModificationException

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

  • 最常见的原因是Collection在上的增强for循环中修改了Collection。仅仅因为您Iterator在源代码中没有看到对象并不意味着就没有对象Iterator!幸运的是,错误for循环的其中一条语句通常会出现在堆栈跟踪中,因此跟踪错误通常很容易。
  • 一个棘手的情况是您的代码将对Collection对象的引用传递给周围。请注意,集合的不可修改视图(例如由产生的Collections.unmodifiableList())保留了对可修改集合的引用,因此在“不可修改”集合上进行迭代会引发异常(修改已在其他地方进行)。您的其他视图Collection例如子列表Map条目集Map键集)也保留了对原始(可修改)的引用Collection。即使对于线程安全的Collection(例如),这也可能是一个问题CopyOnWriteList。不要假设线程安全(并发)集合永远不会抛出异常。
  • Collection在某些情况下,哪些操作可以修改a 可能是意外的。例如,LinkedHashMap.get()修改其collection
  • 最困难的情况是异常由于多个线程同时进行修改而导致的。

编程以防止并发修改错误

在可能的情况下,将所有引用限制在一个Collection对象上,以便更容易防止并发修改。使Collection一个private物体或一个局部变量,也不要到返回引用Collection的方法或它的迭代器。这样一来,检查可以修改的所有地方就容易得多Collection。如果Collection要由多个线程使用,则可以确保Collection通过适当的同步和锁定来仅访问线程。


我想知道为什么在单个线程的情况下不允许并发修改。如果允许单个线程对常规哈希图进行并发修改,会发生什么问题?
MasterJoe

4

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

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

2

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

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

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


我认为它们的意思是Hashtable。它是JDK 1.0的一部分。与Vector一样,它被编写为线程安全的-而且速度很慢。两者都已被非线程安全替代方案:HashMap和ArrayList取代。支付您使用的费用。
duffymo

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

0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.