为什么将ConcurrentModificationException的设计标准仅选择为结构修改

时间:2018-10-30 08:35:31

标签: java collections concurrentmodification design-decisions

我确实知道,在创建迭代器后,只要对基础集合进行结构化修改,就会抛出ConcurrentModificationExcpetion。这似乎是设计决定。

我想知道为什么CME不考虑通过set()方法进行更新?最终,集合将得到更新,并且如果创建了迭代器,遍历仍可能导致结果不一致。

3 个答案:

答案 0 :(得分:2)

除非您确实参与了设计决策,否则不可能知道设计决策的确切原因。

我推测原因可能是迭代器设计为迭代 collection ,而不是迭代collection中的 values。

换句话说,迭代器就像一个“指针”,它将被移动以依次指向集合中的每个“空间”。通过了解构造迭代器时有关集合“形状”的一些知识,您知道如何移动指针以指向集合中的下一个空间。

如果存储在一个空格中的值发生了变化,那并不重要:该空格和集合中的所有其他空格都保持不变。

但是,如果通过结构修改来更改集合的形状,则基本上使关于该指针指向的任何假设无效;除了做一些非常聪明的事情之外,您最好放弃并抛出异常。

答案 1 :(得分:2)

  

最终,集合将得到更新,并且如果创建了迭代器,遍历仍然可能导致结果不一致。

实际上,我认为原因是set操作不会导致结果不一致。迭代集合的代码将以高度可预测的方式看到set调用的修改结果,或者没有看到。并且保证该代码仍然只能看到该集合的每个其他元素(即,除了您“设置”的元素之外)仅一次。

相反,对典型的非并行集合的结构修改可能导致集合条目/元素四处移动。例如:

  • ArrayList中插入或删除会导致后备数组中的元素改变位置。

  • HashMapHashSet中插入会导致调整大小,从而导致哈希链被重建。

在两种情况下,结构上的修改都可能导致元素被跳过或多次出现。

答案 2 :(得分:1)

请在下面找到我对您的假设的评论。

  

我确实知道,在创建迭代器后,只要对基础集合进行结构性修改,就会抛出ConcurrentModificationException

不完全是。实际上,当迭代器尝试获取下一个值并且基础集合中发生结构性更改时,实际上会引发ConcurrentModificationException。 (我想强调一点,即在基础集合上调用ConcurrentModificationExceptionadd时不会引发remove

然后,关于您的最后一个假设:

  

最终,集合将得到更新,并且如果创建了迭代器,遍历仍然可能导致结果不一致。

我认为这是不正确的。遍历如何仍会导致结果不一致?如果您修改列表第i个位置的值,则会立即看到此更改。该列表实际上已修改,但没有结构上修改。仅第i个位置的值发生更改,列表结构(即ArrayList的基础数组不会改变)的大小不会改变,LinkedList不会分配新的大小节点等)。

最后,关于这句话:

  

这似乎是设计决定。

绝对。这是100%的设计决定,虽然我没有参与其中,但我可以肯定,它的目标是避免遍历基础集合时出现不一致的目的。最好以尽早引发异常并让用户知道,而不是以意外或不一致的数据结束。

但是,文档还提到快速失败迭代器会在尽力而为的基础上抛出ConcurrentModificationException

例如,考虑以下代码:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    try {
        Integer next = iterator.next();
        System.out.println("iterator.next() = " + next);

        System.out.println("BEFORE remove: " + list);
        list.remove(0);
        System.out.println("AFTER remove: " + list);

    } catch (ConcurrentModificationException e) {

        System.out.println("CME thrown, ending abnormally...");
        System.exit(1);
    }
}

在这里,我们尝试在遍历列表时删除列表的第一个元素。执行后,您将看到以下输出:

iterator.next() = 1
BEFORE remove: [1, 2, 3]
AFTER remove: [2, 3]
CME thrown, ending abnormally...

这是预期的行为。当迭代器第二次尝试从列表中获取元素时,将引发ConcurrentModificationException。这是因为该实现检测到自创建迭代器以来已进行了结构更改。因此,iterator.next()会引发CME。

但是,由于不能保证 fail-fast 迭代器总是抛出ConcurrentModificationException,而是仅在尽力而为的基础上进行迭代,因此有一种方法可以欺骗实现。考虑下面的代码,它是上一个代码片段的修改版本(如果执行,请注意,直到完成该代码需要一些时间):

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));

int structuralChanges = 0;

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    try {
        Integer next = iterator.next();
        System.out.println("iterator.next() = " + next);

        System.out.println("BEFORE remove: " + list);
        list.remove(0);
        System.out.println("AFTER remove: " + list);
        structuralChanges++;

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            list.add(i);
            structuralChanges++;
            list.remove(list.size() - 1);
            structuralChanges++;
        }
        list.add(0);
        structuralChanges++;

        System.out.println("Structural changes so far = " + structuralChanges);

    } catch (ConcurrentModificationException e) {

        System.out.println("CME NEVER thrown, so you won't see this message");
        System.exit(1);
    }
}

System.out.println("AFTER everything: " + list);
System.out.println("Program ending normally");

在这里,我们再次在迭代时删除列表的第一个元素,但是在迭代器获取下一个元素之前,我们正在做数百万个结构修改。实际上,如果我们考虑Integer.MAX_VALUE * 2 + 1循环内的所有list.add(i)list.remove(list.size() - 1)调用以及立即进行的for调用,我们就在进行list.add(0)结构修改在for循环之后。

此外,我们通过structuralChanges变量跟踪所有结构修改,该变量在每次结构修改后立即递增。

执行上述代码后,您将看到以下输出:

iterator.next() = 1
BEFORE remove: [1, 2, 3]
AFTER remove: [2, 3]
Structural changes so far = 0
iterator.next() = 3
BEFORE remove: [2, 3, 0]
AFTER remove: [3, 0]
Structural changes so far = 0
iterator.next() = 0
BEFORE remove: [3, 0, 0]
AFTER remove: [0, 0]
Structural changes so far = 0
AFTER everything: [0, 0, 0]
Program ending normally

如输出所示,没有抛出ConcurrentModificationException

此外,在完成所有结构修改后,structuralChanges变量的值最终为0。这是因为它是int类型的变量,并且在被0次(由于我们的人工{{1 }}循环并随后递增,由于调用了原始代码Integer.MAX_VALUE * 2 + 2而加上了Integer.MAX_VALUE * 2 + 1

然后,在进行所有这些for结构修改之后,当我们在1循环的后续迭代中调用list.remove(0)时,永远不会抛出Integer.MAX_VALUE * 2 + 2。我们欺骗了实现,因此现在我们可以内部查看数据发生了什么。 (在内部,该实现通过保持iterator.next()计数来跟踪结构修改,就像我对while变量所做的那样。)