【问题标题】:Detecting concurrent modifications?检测并发修改?
【发布时间】:2023-03-16 19:13:01
【问题描述】:

在我正在处理的多线程应用程序中,我们偶尔会在列表中看到ConcurrentModificationExceptions(主要是ArrayList,有时是向量)。但在其他时候,我认为正在发生并发修改,因为迭代集合似乎缺少项目,但没有抛出异常。我知道ConcurrentModificationException 的文档说您不能依赖它,但是我将如何确保我不会同时修改列表?并且将每个对集合的访问包装在同步块中是防止它的唯一方法吗?

更新:是的,我知道Collections.synchronizedCollection,但它并不能防止有人在您迭代集合时修改它。我认为,当有人在我迭代集合时向集合中添加某些内容时,至少会出现一些问题。

第二次更新如果有人想像 Jason 那样提到 synchronizedCollection 和克隆,同时提到 java.util.concurrent 和 jacekfoo 和 Javamann 那样的 apache 集合框架,我可以接受答案。

【问题讨论】:

  • RE 更新:在迭代之前使用同步集合和复制列表内容的组合是必要的。
  • 杰森,是的,我现在明白了。如果您要在答案中提及 CopyOnWriteArrayList,我会接受。
  • 当你迭代时,列表的“实时”版本没有被修改是否重要,或者你可以迭代一个可能陈旧的副本?我认为可能还有一个设计问题(谁控制对列表的访问?),以及同步和 java.util.concurrent 的使用。
  • OK Paul -- 将您要求的项目添加到我的回答中。

标签: java collections


【解决方案1】:

根据您的更新频率,我最喜欢的一种是 CopyOnWriteArrayList 或 CopyOnWriteArraySet。他们在更新时创建一个新列表/集,以避免并发修改异常。

【讨论】:

    【解决方案2】:

    您最初的问题似乎是要求一个迭代器,该迭代器可以查看底层集合的实时更新,同时保持线程安全。在一般情况下,这是一个非常昂贵的问题,这就是为什么标准集合类都没有这样做的原因。

    有很多方法可以部分解决问题,在您的应用程序中,其中一种可能就足够了。

    Jason 给了a specific way to achieve thread safety, and to avoid throwing a ConcurrentModificationException,但只是以牺牲活力为代价。

    Javamann 在java.util.concurrent 包中提到two specific classes 以无锁方式解决了相同的问题,其中可扩展性至关重要。这些仅随 Java 5 一起提供,但已经有各种项目将包的功能反向移植到早期的 Java 版本中,包括 this one,尽管它们在早期的 JRE 中不会有这么好的性能。

    如果您已经在使用某些 Apache Commons 库,那么作为 jacekfoo points outapache collections framework 包含一些有用的类。

    您也可以考虑查看Google collections framework

    【讨论】:

    • 您应该补充一点,还有一个可用的 java.util.concurrent 包的反向移植。
    【解决方案3】:

    查看 java.util.concurrent 了解旨在更好地处理并发的标准 Collections 类的版本。

    【讨论】:

      【解决方案4】:

      是的,您必须同步对集合对象的访问。

      或者,您可以在任何现有对象周围使用同步包装器。见Collections.synchronizedCollection()。例如:

      List<String> safeList = Collections.synchronizedList( originalList );
      

      但是所有代码都需要使用安全版本,即使这样在另一个线程修改时进行迭代也会导致问题。

      要解决迭代问题,先复制列表。示例:

      for ( String el : safeList.clone() )
      { ... }
      

      有关更优化的线程安全集合,另请查看java.util.concurrent

      【讨论】:

      • 您的意思是 Collections.synchronizedList(),对吧?没有 Collections.consurrentList()。
      • ArrayList 在内部使用迭代器进行复制。用一个包裹原始列表会减少问题的窗口,但不会消除它。
      • 谢谢,解决这两个问题。如果您已同步,则 clone() 是安全的。
      • 另外,考虑从 getter 方法返回 Collections.unmodifiableList()。这样,所有从您的类中获取集合的代码都会获取该集合的不可变快照。在您的班级中,您可以使用适当的同步来避免竞争条件。
      • .clone() 如果 safeList 是 List 将无法编译,并且您无法转换 Collections.synchronizedList() 返回的 List,因为它不可克隆。
      【解决方案5】:

      如果您在迭代列表时尝试从列表中删除元素,通常会收到 ConcurrentModificationException。

      最简单的测试方法是:

      List<Blah> list = new ArrayList<Blah>();
      for (Blah blah : list) {
           list.remove(blah); // will throw the exception
      }
      

      我不确定你会如何解决它。您可能必须实现自己的线程安全列表,或者您可以创建原始列表的副本以进行写入,并拥有一个写入列表的同步类。

      【讨论】:

      • 即使你只有一个线程也会出现这个问题。您可以通过任何一种方式避免它; 1)在迭代之前获取列表的副本,2)使用 Iterator.remove() 执行删除。注意:并发集合在这里不会抛出异常。
      【解决方案6】:

      您可以尝试使用防御性复制,以便对 List 的修改不会影响其他人。

      【讨论】:

        【解决方案7】:

        在同步块中包装对集合的访问是正确的方法。标准编程实践要求在处理跨多个线程共享的状态时使用某种锁定机制(信号量、互斥锁等)。

        但是,根据您的用例,您通常可以进行一些优化以仅在某些情况下锁定。例如,如果您有一个经常读取但很少写入的集合,那么您可以允许并发读取但在写入进行时强制执行锁定。仅当集合处于修改过程中时,并发读取才会导致冲突。

        【讨论】:

          【解决方案8】:

          ConcurrentModificationException 是尽力而为,因为您要问的是一个难题。除了证明您的访问模式不会同时修改列表之外,没有什么好的方法可以在不牺牲性能的情况下可靠地做到这一点。

          同步可能会阻止并发修改,这可能是您最终会采用的方法,但最终可能代价高昂。最好的办法可能是坐下来思考一下你的算法。如果您无法提出无锁解决方案,请使用同步。

          【讨论】:

            【解决方案9】:

            查看实现。它基本上存储一个int:

            transient volatile int modCount;
            

            当有“结构修改”(如删除)时,它会增加。如果迭代器检测到 modCount 发生了变化,它会抛出并发修改异常。

            同步(通过 Collections.synchronizedXXX)不会有好处,因为它不能保证迭代器的安全,它只会通过 put、get、set ... 同步写入和读取。

            参见 java.util.concurennt 和 apache 集合框架(当读取(非同步)多于写入时,它有一些经过优化的类在并发环境中可以正常工作 - 参见 FastHashMap。

            【讨论】:

              【解决方案10】:

              您还可以在列表上的迭代中进行同步。

              List<String> safeList = Collections.synchronizedList( originalList );
              
              public void doSomething() {
                 synchronized(safeList){
                   for(String s : safeList){
                         System.out.println(s);
              
                   }
                 }
              
              }
              

              这将在同步时锁定列表,并在您编辑或迭代列表时阻止所有尝试访问该列表的线程。缺点是会造成瓶颈。

              这比 .clone() 方法节省了一些内存,并且可能会更快,具体取决于您在迭代中执行的操作...

              【讨论】:

                【解决方案11】:

                Collections.synchronizedList() 将呈现一个名义上线程安全的列表,而 java.util.concurrent 具有更强大的功能。

                【讨论】:

                  【解决方案12】:

                  这将摆脱您的并发修改异常。但是,我不会谈论效率;)

                  List<Blah> list = fillMyList();
                  List<Blah> temp = new ArrayList<Blah>();
                  for (Blah blah : list) {
                       //list.remove(blah);  would throw the exception
                       temp.add(blah);
                  }
                  list.removeAll(temp);
                  

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2020-10-05
                    • 2012-05-28
                    • 2011-11-15
                    • 2013-09-22
                    • 1970-01-01
                    • 2016-11-19
                    • 2011-10-04
                    相关资源
                    最近更新 更多