【问题标题】:Why is a ConcurrentModificationException thrown and how to debug it为什么会抛出 ConcurrentModificationException 以及如何调试它
【发布时间】:2010-10-10 19:10:51
【问题描述】:

我正在使用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)

【问题讨论】:

  • 你能提供更多的上下文吗?您是否正在合并、更新或删除实体?这个实体有什么关联?你的级联设置呢?
  • 从堆栈跟踪中您可以看到在遍历 HashMap 时发生了异常。肯定有其他线程正在修改地图,但异常发生在正在迭代的线程中。

标签: java exception collections concurrentmodification


【解决方案1】:

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

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

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

【讨论】:

  • 我认为他们的意思是哈希表。它作为 JDK 1.0 的一部分提供。像 Vector 一样,它被编写成线程安全的——而且速度很慢。两者都已被非线程安全的替代方案所取代:HashMap 和 ArrayList。为你使用的东西付费。
【解决方案2】:

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

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() 操作。

【讨论】:

  • 可能,但看起来 Hibernate 正在执行迭代,应该合理正确地实现。可能会有回调修改地图,但这不太可能。不可预测性指向实际的并发问题。
  • 该异常与线程并发无关,是迭代器的后备存储被修改引起的。是否通过另一个线程对迭代器无关紧要。恕我直言,这是一个名称不佳的异常,因为它给人的原因是错误的印象。
  • 我同意,但是,如果它是不可预测的,则很可能是线程问题导致发生此异常的条件。由于异常名称,这使得它更加混乱。
  • 这是正确的,比接受的答案更好的解释,但接受的答案是一个很好的解决方法。 ConcurrentHashMap 不受 CME 约束,即使在迭代器内部也是如此(尽管迭代器仍然是为单线程访问而设计的)。
  • 这个解决方案没有意义,因为地图没有 iterator() 方法。罗宾的例子将适用于例如列表。
【解决方案3】:

根据您要执行的操作尝试 CopyOnWriteArrayList 或 CopyOnWriteArraySet。

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • 真的解决了问题吗?我遇到了同样的问题,但我绝对可以排除任何线程问题。
    • 另一种解决方案是创建地图的副本并迭代该副本。或者复制一组键并遍历它们,从原始映射中获取每个键的值。
    • 即时救星。将研究为什么这会如此有效,这样我就不会在未来得到更多惊喜。
    • 我猜它不是同步问题,如果在循环同一个对象时修改相同的修改是问题。
    • 来自 ConcurrentHashMap Javadoc Similarly, 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.
    【解决方案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();
        }
    

    【讨论】:

      【解决方案6】:

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

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

      ConcurrentModificationException 的文档说:

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

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

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

      注意

      HashSetHashMapTreeSetArrayList 类的文档是这样说的:

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

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

      再次注意,行为“无法得到保证”,只是“尽最大努力”。

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

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

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

      调试ConcurrentModificationException

      因此,当您看到由于ConcurrentModificationException 导致的堆栈跟踪时,您不能立即假设原因是对Collection 的多线程访问不安全。您必须examine the stack-trace 来确定@​​987654372@ 的哪个类抛出了异常(该类的某个方法将直接或间接抛出它),以及针对哪个Collection 对象。然后你必须检查从哪里可以修改该对象。

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

      防止并发修改错误的编程

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

      【讨论】:

      • 我想知道为什么在单线程的情况下不允许并发修改。如果允许单个线程对常规哈希映射进行并发修改,会出现什么问题?
      【解决方案7】:

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

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

      【讨论】:

      • 美丽。这应该到顶部。是 O(n) 时间,O(1) 空间吗?
      • 这是更优雅的解决方案。谢谢。
      【解决方案8】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-09-11
        • 2011-08-18
        • 2011-08-29
        • 2012-08-16
        相关资源
        最近更新 更多