【问题标题】:Why iterator.remove does not throw ConcurrentModificationException为什么 iterator.remove 不抛出 ConcurrentModificationException
【发布时间】:2014-09-11 11:15:44
【问题描述】:

iterator.remove()list.remove() 有何不同,因此迭代器不会抛出异常而list.remove() 会抛出异常?最后,两者都在修改集合大小。

请忽略这里的多线程。我只是在谈论一个 for-each 循环和一个迭代器循环。据我所知,for-each 循环仅在内部创建一个迭代器。

我很困惑。

【问题讨论】:

    标签: java collections foreach iterator


    【解决方案1】:

    我想你的意思是,如果你正在迭代一个列表,为什么 list.remove() 会导致 ConcurrentModificationException 被抛出而 iterator.remove() 不会?

    考虑这个例子:

        List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
    
        for (Iterator<String> iter = list.iterator(); iter.hasNext(); ) {
            if (iter.next().equals("b")) {
                // iter.remove();    // #1
                // list.remove("b"); // #2
            }
        }
    

    如果您取消注释第 1 行,它将正常工作。如果您取消注释第 2 行(但保留第 1 行注释),则会导致随后对 iter.next() 的调用抛出 ConcurrentModificationException

    原因是迭代器是一个单独的对象,它对底层列表的内部状态有一些引用。如果在迭代器运行时修改列表,可能会导致迭代器表现不佳,例如通过跳过元素、重复元素、索引数组末尾等。它会尝试检测此类修改,因此如果检测到,它会抛出 ConcurrentModificationException

    通过迭代器移除元素有效且不会导致异常,因为这会更新底层列表引用列表内部的迭代器状态,因此一切都可以保持一致。

    但是,iterator.remove() 并没有什么特别之处,可以让它在所有情况下都能正常工作。如果有多个迭代器在同一个列表上进行迭代,那么其中一个进行的修改会给其他人带来问题。考虑:

        Iterator<String> i1 = list.iterator();
        Iterator<String> i2 = list.iterator();
        i1.remove();
        i2.remove();
    

    我们现在有两个迭代器指向同一个列表。如果我们使用其中一个来修改列表,它会破坏第二个的操作,因此对i2.remove() 的调用将导致ConcurrentModificationException

    【讨论】:

    • 这个其实是比较正确的答案
    • 这是最好的解释。
    • 很好的解释。这应该是正确的答案。
    • 是对行为实现的解释(在各种集合类中)。但这种行为的原因是Iterator设计 以这种方式工作......并且指定 以这种方式工作。设计/规范促成了实施,而不是相反。
    • @StephenC 在这种情况下,设计和规范是由实施考虑因素决定的。当然,Iterator 被设计和指定为按照它的方式工作......但是为什么呢?答案是它是可预测性、可靠性、易于实施和易于使用之间的权衡。很容易考虑一个具有更强保证的规范,但这会给实现带来繁重的要求。我可以进一步解释,但这个评论越来越长。如果您需要更多解释,请随时提出问题。 :-)
    【解决方案2】:

    ConcurrentModificationException 不会被 Iterator.remove() 抛出,因为这是在迭代时修改集合的允许方式。这就是javadoc for Iterator 所说的:

    从底层集合中移除此迭代器返回的最后一个元素(可选操作)。每次调用 next() 时只能调用一次此方法。 如果在迭代过程中通过调用此方法以外的任何方式修改了底层集合,则迭代器的行为是未指定的。

    如果您以任何其他方式更改正在迭代的集合,那么您可能会遇到异常,具体取决于迭代器的实现以及您正在迭代的集合(或其他)。 (一些集合类不会给你ConcurrentModificationException:检查各自的 javadocs 以了解它们如何指定 它们的迭代器的行为)

    如果您在同一个集合上有两个迭代器,并且您通过其中一个删除,您也可能会遇到异常。


    iterator.remove 与 list.remove 有何不同之处在于迭代器不会抛出异常而 list.remove 会抛出异常?

    原因 #1。如果您从同一调用堆栈的两个位置同时更新一个非并发集合,则该行为将破坏迭代的设计不变量1。保证非并发集合的迭代只能看到集合中的所有元素一次。 (相比之下,在并发集合中,这些保证被​​放宽了。)

    原因 #2。非并发集合类型未实现为线程安全的。因此,如果集合和迭代器用于由不同线程更新集合,您可能会遇到竞争条件和内存异常。这不是 strong 原因,因为无论如何您都会遇到这些问题。但是,以两种不同的方式进行更新会使问题变得更糟。


    我只是在谈论 for-each 循环和迭代器循环。据我所知,for-each 循环仅在内部创建迭代器。

    没错。 for-each 循环实际上只是使用迭代器的 while 循环的语法糖。

    另一方面,如果你使用这样的循环:

        for (int i = 0; i < list.size(); i++) {
            if (...) {
                list.remove(i);
            }
        }
    

    你不会得到ConcurrentModificationException,但是你需要为你删除的元素调整索引变量,另一个线程的更新可能会导致你跳过元素或多次访问它们 2.


    1 - 为了实现“恰好一次”的迭代行为,当您通过集合对象删除元素时,需要更新迭代器数据结构以使其与集合发生的情况保持同步。这在当前实现中是不可能的,因为它们不保持与未完成迭代器的链接。如果他们这样做了,他们将需要使用 Reference 对象或冒内存泄漏的风险。

    2 - 甚至获得IndexOutOfBoundsException。如果集合不是并发/正确同步的,你可能会遇到更严重的问题。

    【讨论】:

      【解决方案3】:

      因为抛出异常的是迭代器。如果你打电话给List.remove(),它不知道删除,只是它脚下发生了一些变化。如果您调用Iterator.remove(),它会知道当前元素已被删除以及如何处理它。

      【讨论】:

        【解决方案4】:

        下面是一个例子,如果集合迭代器不检查底层集合的修改,事情会如何出错。 ArrayLists的迭代器是这样实现的:

        private class Itr implements Iterator<E> {
            int cursor;       // index of next element to return
            int lastRet = -1; // index of last element returned; -1 if no such
        
            public E next() {
                checkForComodification();
                int i = cursor;
                if (i >= size) throw new NoSuchElementException();
                // ...
                cursor = i + 1;
                return (E) elementData[lastRet = i];
            }
        
            public void remove() {
                // ...
                ArrayList.this.remove(lastRet);
                // ...
                cursor = lastRet;
                lastRet = -1;
            }
        

        我们来看一个例子:

        List list = new ArrayList(Arrays.asList(1, 2, 3, 4));
        Iterator it = list.iterator();
        Integer item = it.next();
        

        我们删除第一个元素

        list.remove(0);
        

        如果我们现在想调用it.remove(),迭代器将删除number 2,因为这是lastRet 现在指向的字段。

        if (item == 1) {
           it.remove(); // list contains 3, 4
        }
        

        这将是不正确的行为!迭代器的合约声明remove() 删除了next() 返回的最后一个元素,但在存在并发修改的情况下它无法保持其合约。因此它选择安全起见并抛出异常。

        其他集合的情况可能更复杂。如果您修改HashMap,它可能会根据需要增长或缩小。那时,元素会落到不同的桶中,并且在重新散列之前保持指向桶的指针的迭代器将完全丢失。

        请注意,iterator.remove() 自身不会引发异常,因为它能够更新自身和集合的内部状态。但是,在同一实例集合的两个迭代器上调用 remove() 会抛出异常,因为它会使其中一个迭代器处于不一致的状态。

        【讨论】:

          【解决方案5】:
          public class ArrayListExceptionTest {
              public static void main(String[] args) {
                  ArrayList<String> list1 = new ArrayList<>();
                  list1.add("a");
                  list1.add("b");
                  list1.add("c");
                  Iterator<String> it1 = list1.iterator();
                  ArrayList<String> list2 = new ArrayList<String>();
                  list2.add("a");
                  try {
          
                      while (it1.hasNext()) {
                          list1.add(it1.next());
                      }
                  } catch (ConcurrentModificationException e) {
                      e.printStackTrace();
                  }
                  it1 = list1.iterator();
                  while (it1.hasNext()) {
                      System.out.println(it1.next());
                  }
                  it1 = list1.iterator();
                  try {
                      while (it1.hasNext()) {
                          if (it1.next().equals("a"))
                              list1.retainAll(list2);
                      }
          
                  } catch (ConcurrentModificationException e) {
                      e.printStackTrace();
                  }
                  it1 = list1.iterator();
                  while (it1.hasNext()) {
                      System.out.println(it1.next());
                  }
                  it1 = list1.iterator();
                  Iterator<String> it2 = list1.iterator();
                  it1.remove();
                  it2.remove();
              }
          }
          

          以上3种情况可以看

          案例1:通过添加元素进行修改,因此当使用next()函数时会导致ConcurrentModificationException。

          案例2:使用retain()进行修改,因此使用next()函数时会导致ConcurrentModificationException。

          案例 3:将抛出 java.lang.IllegalStateException 而不是 ConcurrentModificationException。

          输出:

          a
          b
          c
          a
          
          a
          a
          
              java.util.ConcurrentModificationException
                  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
                  at java.util.ArrayList$Itr.next(ArrayList.java:859)
                  at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.java:21)
              java.util.ConcurrentModificationException
                  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
                  at java.util.ArrayList$Itr.next(ArrayList.java:859)
                  at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.java:37)
              Exception in thread "main" java.lang.IllegalStateException
                  at java.util.ArrayList$Itr.remove(ArrayList.java:872)
                  at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.java:55)
          

          【讨论】:

            【解决方案6】:

            用一些额外的低级细节回答这个问题:

            在迭代期间下一次调用 next() 方法时抛出 ConcurrentModificationException。

            所以它不是抛出这个异常的集合的 remove() 方法,而是它的迭代器实现的 next() 方法。

            Exception in thread "main" java.util.ConcurrentModificationException
                at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
                at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
                at Collection.IteratorDemo.main(IteratorDemo.java:16)
            

            您可以查看上述错误日志中的第 3 行。

              List<Integer> nums = new ArrayList<>();
                 nums.add(1);
                 nums.add(2);
                 for(int i : nums){
                    nums.remove(1);
                    System.out.println(i);
             }
            

            这个 next() 方法如何知道集合是否被修改?

            通过检查变量,AbstractList

             protected transient int modCount = 0;
            

            此变量通过在对集合的添加/删除调用中递增和递减值来维护集合的结构更改。 这就是集合实现快速失败迭代器的方式。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2011-08-29
              • 1970-01-01
              • 2018-01-05
              • 2015-09-01
              • 1970-01-01
              • 2020-05-17
              • 2013-02-12
              • 2011-08-18
              相关资源
              最近更新 更多