【问题标题】:Why doesn't this code throw a ConcurrentModificationException?为什么这段代码不抛出 ConcurrentModificationException?
【发布时间】:2015-04-18 22:10:23
【问题描述】:

为什么这段代码不抛出ConcurrentModificationException?它在迭代 Collection 时修改它,而不使用 Iterator.remove() 方法,即 the only safe way of removing

List<String> strings = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String string : strings)
    if ("B".equals(string))
        strings.remove("B");
System.out.println(strings);

如果我将ArrayList 替换为LinkedList,我会得到相同的结果。但是,如果我将列表更改为("A", "B", "C", "D) 或只是("A", "B"),我会得到预期的异常。到底是怎么回事?如果相关,我正在使用jdk1.8.0_25

编辑

我找到了以下链接

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4902078

相关部分是

简单的解决方案是在 AbstractList 中的 hasNext 中添加共修改检查,但这会使共修改检查的成本加倍。 事实证明,只在最后一次进行测试就足够了 迭代,这几乎不会增加成本。换句话说, hasNext 的当前实现:

    public boolean hasNext() {
        return nextIndex() < size;
    }

被这个实现取代:

    public boolean hasNext() {
        if (cursor != size())
            return true;
        checkForComodification();
        return false;
    }

不会进行此更改,因为 Sun 内部监管机构拒绝了它。正式裁决表明,这一变化“已经 证明了具有重大兼容性影响的潜力 在现有代码上。”(“兼容性影响”是该修复程序具有 有可能将沉默的不当行为替换为 ConcurrentModificationException。)

【问题讨论】:

  • 因为ConcurrentModificationException 是在“尽力而为”的基础上抛出的
  • 我喜欢 Sun 没有做出改变的原因是它可能会使一些糟糕的代码实际上开始抛出它应该抛出的异常

标签: java


【解决方案1】:

作为一般规则,ConcurrentModificationExceptions 在检测到而不是引起修改时被抛出。如果你在修改后从不访问迭代器,它不会抛出异常。不幸的是,这个微小的细节使得ConcurrentModificationExceptions 检测数据结构的滥用变得相当不可靠,因为它们只有在损坏完成后才会被抛出。

此方案不会抛出 ConcurrentModificationException,因为在修改后不会在创建的迭代器上调用 next()

for-each 循环实际上是迭代器,因此您的代码实际上如下所示:

List<String> strings = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> iter = strings.iterator();
while(iter.hasNext()){
    String string = iter.next();
    if ("B".equals(string))
        strings.remove("B");
}
System.out.println(strings);

考虑您的代码在您提供的列表上运行。迭代看起来像:

  1. hasNext() 返回 true,进入循环,-> iter 移动到索引 0,string = "A",未删除
  2. hasNext() 返回 true,继续循环 -> iter 移动到索引 1,string = "B",已删除。 strings 现在的长度为 2。
  3. hasNext() 返回 false(iter 当前在最后一个索引处,没有更多索引可走),退出循环。

因此,当对next() 的调用检测到已进行修改时,会抛出ConcurrentModificationExceptions,这种情况几乎可以避免此类异常。

对于您的其他两个结果,我们确实得到了例外。对于"A", "B", "C", "D",删除“B”后,我们仍在循环中,next() 检测到ConcurrentModificationException,而对于"A", "B",我想它是某种 ArrayIndexOutOfBounds 被捕获并重新抛出为ConcurrentModificationException

【讨论】:

  • 但是如果他们只是检查hasNext()而不是next()中的并发修改,肯定会被抛出?这是如何“尽力而为”的?
  • @pbabcdefp 这是“尽力而为”,因为如果修改不同步,则无法保证 JRE 会注意到并发修改。
  • @pbabcdefp: "Best-effort basis" 并不意味着“我们会尽力而为,尽我们所能”,就像您可能想的那样。这意味着他们会尝试,但他们不会做出任何承诺。
  • @user2357112 是正确的,这是用词不当。它实际上意味着“合理的努力”,而不是“尽力而为”。
  • @user2357112supportsMonica - 你能帮我解决这个相关问题吗? stackoverflow.com/questions/61421763/… 谢谢。
【解决方案2】:

ArrayList 的迭代器中的hasNext 只是

public boolean hasNext() {
    return cursor != size;
}

remove 调用后,迭代器在索引 2 处,列表的大小为 2,因此报告迭代完成。没有并发修改检查。使用 ("A", "B", "C", "D) 或 ("A", "B"),迭代器不在列表的新末尾,因此调用 next,并抛出例外。

ConcurrentModificationExceptions 只是一个调试帮助。你不能依赖它们。

【讨论】:

    【解决方案3】:

    @Tavian Barnes 完全正确。如果有问题的并发修改不同步,则不能保证抛出此异常。引用java.util.ConcurrentModification 规范:

    请注意,通常无法保证快速失败的行为 说,不可能在存在的情况下做出任何硬性保证 不同步的并发修改。快速失败的操作抛出 ConcurrentModificationException 尽最大努力。因此,它 编写一个依赖此异常的程序是错误的 其正确性:仅应使用 ConcurrentModificationException 检测错误。

    Link to JavaDoc for ConcurrentModificationException

    【讨论】:

    • 请注意,原帖中的示例代码是单线程的。这里没有同步问题。 ConcurrentModificationException 中的“并发”与线程无关(直接);它只是意味着“在进行迭代的同时”,而不是“另一个线程修改了列表”。
    猜你喜欢
    • 1970-01-01
    • 2011-08-18
    • 2012-11-25
    • 2013-11-13
    • 1970-01-01
    • 1970-01-01
    • 2013-01-18
    • 2021-07-24
    • 1970-01-01
    相关资源
    最近更新 更多