我确实明白,只要在创建迭代器后对基础集合进行结构修改,就会引发 ConcurrentModificationExcpetion。这似乎是一个设计决定。
我想知道为什么 CME 不考虑通过 set() 方法进行更新?最终,集合正在更新,如果创建了迭代器,遍历仍然可能导致结果不一致。
我确实明白,只要在创建迭代器后对基础集合进行结构修改,就会引发 ConcurrentModificationExcpetion。这似乎是一个设计决定。
我想知道为什么 CME 不考虑通过 set() 方法进行更新?最终,集合正在更新,如果创建了迭代器,遍历仍然可能导致结果不一致。
除非您实际参与制定设计决策,否则不可能知道设计决策的确切原因。
我推测原因可能是迭代器旨在迭代集合,而不是集合中的值。
换句话说,迭代器就像一个“指针”,它将被移动以依次指向集合中的每个“空间”。通过在构造迭代器时了解集合的“形状”,您就知道如何移动指针以指向集合中的下一个空间。
如果存储在空间中的值发生变化,这并不重要:该空间和集合中的所有其他空间都保持不变。
但是,如果您通过结构修改来更改集合的形状,那么您基本上会使您对该指针指向的位置的任何假设无效;没有做一些非常聪明的事情,你还不如放弃并抛出异常。
最终,集合正在更新,如果创建了迭代器,遍历仍然可能导致结果不一致。
实际上,我认为原因是set
操作不会导致结果不一致。迭代集合的代码将以set
高度可预测的方式看到调用修改的结果……或者看不到……。并且保证代码仍然可以看到集合中的其他元素(即除了您“设置”的元素之外)恰好一次。
相比之下,对典型非并发集合的结构修改会导致集合条目/元素四处移动。例如:
an 中的插入或删除ArrayList
会导致后备数组中的元素更改位置。
HashMap
在or中的插入HashSet
可能会导致调整大小,从而导致哈希链被重建。
在这两种情况下,结构修改都可能导致元素被跳过或多次看到。
请在下面找到我对您的假设的评论。
我确实理解,
ConcurrentModificationException
只要在创建迭代器后对基础集合进行结构修改,就会引发这种情况。
不完全是。当ConcurrentModificationException
迭代器尝试获取下一个值并且基础集合中发生结构更改时,实际上会抛出 。(我想强调的ConcurrentModificationException
是,在调用 ieadd
或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
当迭代器第二次尝试从列表中获取元素时抛出A。这是因为实现检测到自迭代器创建以来已经进行了结构更改。因此,iterator.next()
抛出 CME。
但是,由于不能保证快速失败的迭代器总是抛出 a 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");
在这里,我们在迭代时再次删除列表的第一个元素,但在迭代器获取下一个元素之前,我们正在进行数百万次结构修改。事实上,如果我们考虑循环内的所有and调用,以及紧接在循环之后的调用,我们正在进行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
Integer.MAX_VALUE * 2 + 2
Integer.MAX_VALUE * 2 + 1
for
1
list.remove(0)
然后,在所有这些Integer.MAX_VALUE * 2 + 2
结构修改之后,当我们调用循环iterator.next()
的以下迭代时,永远while
不会抛出 no 。ConcurrentModificationException
我们已经欺骗了实现,所以我们现在可以看到内部数据发生了什么。(在内部,实现通过保持int
计数来跟踪结构修改,就像我对structuralChanges
变量所做的那样)。