【问题标题】:Concurrent Modification in JavaJava 中的并发修改
【发布时间】:2014-09-07 21:03:39
【问题描述】:

如果我写

String myFirstString = "a";
String mySecondString = "b";

List<String> lstOfStrings = new ArrayList<String>();
lstOfStrings.add(myFirstString);
lstOfStrings.add(mySecondString);

for (String value : lstOfStrings) {
    if(value.equals("a")) {
        lstOfStrings.remove("a");
        System.out.println("removed successfully");
    }
}

它工作正常,但是,

如果我改变 list 中的插入顺序,它会给出 java.util.ConcurrentModificationException 见下面的代码

String myFirstString = "a";
String mySecondString = "b";

List<String> lstOfStrings = new ArrayList<String>();
lstOfStrings.add(mySecondString);
lstOfStrings.add(myFirstString);

for (String value : lstOfStrings) {
    if(value.equals("a")) {
        lstOfStrings.remove("a");
        System.out.println("removed successfully");
    }
}

给出 java.util.ConcurrentModificationException

为什么每个人都会出现这种行为?我知道有很多方法,如 Iterator、CoppyOnWriteArraylist 作为 ConcurrentModificationException 异常的解决方法。但我想知道这个具体案例的原因。请解释一下。

【问题讨论】:

标签: java


【解决方案1】:

我猜测为 for (val:collection) 的迭代器语法糖生成的字节码的实现包括检查您是否在集合的最后一个元素上并且不打扰输入的优化再次迭代器。因此,如果您删除集合中最后一项旁边的项目,它不会抛出异常而只是跳过最后一项。两个实验在一定程度上证明了这一点: 将第三项添加到您的第一个示例中,它将失败。 修改它以删除第 2 项而不是第 1 项,并将再次“成功”完成。

更新:啊,这个问题是重复的 Why isn't this code causing a ConcurrentModificationException?

【讨论】:

    【解决方案2】:

    Java for-each 循环在内部使用集合的iterator

    Java iterator 的行为是 fail-fast,当在循环中更改集合的任何内容时,它会失败。

    有关更多信息,我建议阅读以下文章。

    http://www.jguru.com/faq/view.jsp?EID=221988

    http://www.developersfusion.com/Articles/AD/F/53/ConcurrentModificationException---Fail-Fast-and-Fail-Safe-iterators.aspx

    http://docs.oracle.com/javase/6/docs/api/java/util/ConcurrentModificationException.html

    【讨论】:

    • 根本不回答这个问题。问题是——为什么它发生在一个例子中而不是另一个例子中。
    • @MK 嗯,你是对的。我错过了问题的关键点。您的答案是实际答案,而我的只是附加信息。
    【解决方案3】:

    这是因为ArrayList的迭代器的实现方式。

    当您删除最后一个元素之前的元素时,迭代器将不再迭代最后一个元素。

    尝试打印出每次迭代

    String myFirstString = "a";
    String mySecondString ="b";
    List<String> lstOfStrings = new ArrayList<String>();
    
    lstOfStrings.add(myFirstString);
    lstOfStrings.add(mySecondString);
    
    for (String value : lstOfStrings) {
        System.out.println("Iterating: " + value);
    
        if(value.equals("a")){
            lstOfStrings.remove("a");
            System.out.println("removed successfully");
        }
    }
    
    System.out.println(lstOfStrings);
    

    输出是

    Iterating: a
    removed successfully
    [b]
    

    正如你在最后一个System.out.println中看到的lstOfString仍然包含元素“b”,但它没有被迭代。

    当您查看 ArrayList 的实现时,您会明白原因。

    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
        int expectedModCount = modCount;
    
        public boolean hasNext() {
            return cursor != size;
        }
        ....
    }
    

    Iterator.hasNext() 只检查实际光标是否不是大小。因此,如果您遍历列表并删除字符串"a",则光标指向第二个元素"b",但大小已减小到1。因此第二个元素将不再被迭代。

    即使在 JDK 1.7 中也会发生这种情况,我还没有测试过这种行为在 1.8 中是否仍然存在。

    当您使用Iterator.remove() 方法时,它会起作用,因为迭代器知道删除。

    Iterator<String> iterator = lstOfStrings.iterator();
        while (iterator.hasNext()) {
            String value = iterator.next();
            System.out.println("Iterating: " + value);
            if (value.equals("a")) {
                iterator.remove();
                System.out.println("removed successfully");
            }
        }
    

    打印出来:

    Iterating: a
    removed successfully
    Iterating: b
    

    【讨论】:

      【解决方案4】:

      在您的第一个代码块 sn-p 中没有异常,因为循环将在 lstOfStrings.remove("a") 之后和 interator.next() 调用之前中断。 ArrayList 的迭代器通过检查return cursor != size 来检查hasNext(),其中光标是迭代器中的当前位置,这意味着从列表中删除元素:a 后,光标增加:cursor++(cursor==1) ,但尺寸减小了(您删除了一个元素,size == 1)。它不会抛出异常,但并不意味着它是正确的。

      想想下面的代码:

          for (String value : lstOfStrings) {
          System.out.println("Iterating: " + value);
          if(value.equals("a")){
              lstOfStrings.remove("a");
              System.out.println("removed successfully");
          } else if (value.equals("b")) {
              doSomethingOnB(value) // this won't be executed anyway!!!
          }
      }
      

      如果你在第一个代码 sn-p 中的lstOfStrings 中再添加一个元素,例如:lstOfStrings.add("c"),它会抛出你想要的异常。 :-)

      ConcurrentModificationException 被抛出到您的第二个代码块 sn-p 中,因为它通过了 hasNext() 检查(光标 == 2,大小 == 1),然后进入 interator.next() 调用,这将检查 ArrayList 中的 modCount 和 Iterator 中的 expectedModCount,它们表示对 List 元素大小的修改计数。显然,modeCount == 3(2 个添加和 1 个删除),但是 ArrayList.remove(Object) 方法不会影响 ArrayList.Iterator 中的 expectedModCount,因此 expectedModCount == 2,这就是它抛出异常的原因。

      我不确定是否应该将其视为 ArrayList 中的错误,在您的情况下,根本原因是 ArrayList 的元素修改计数与迭代器的不同步。

      【讨论】:

        【解决方案5】:

        ArrayList API 的一些摘录 -

        1. 列表 modCount 的每次添加和删除都会增加。

        2. 对于每个循环的迭代器调用 next() 方法以获得列表大小 + 1 次,在这种情况下为 3 次。

        3. 当 lstOfStrings.remove("a");被称为 modCount 设置为 3。

        4. 在迭代器 checkForComodification() 的每个 next() 迭代中;调用来检查是否有任何修改,因为发现修改我们得到 ConcurrentModificationException。

        Iterator 为什么不抛出异常呢?

        是否有调用 checkForComodification();在这个 ArrayList.this.remove(lastRet);它会抛出同样的异常。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-10-29
          相关资源
          最近更新 更多