【问题标题】:For loop vs. Iterator to avoid ConcurrentModificationException with an ArrayListFor 循环与 Iterator 避免使用 ArrayList 的 ConcurrentModificationException
【发布时间】:2022-12-14 01:05:38
【问题描述】:

问题:在 ArrayList 中添加、删除、修改项目的最佳(性能方面)解决方案是什么,同时避免在操作期间抛出 ConcurrentModificationException

语境:根据我对这个问题的研究,手头的问题似乎没有任何直接的答案 - 大多数建议使用CopyOnWriteArrayList,但我的理解是不建议对于大型数组列表(我正在使用它,因此是问题的性能方面)。

因此,我的理解可以总结如下,但要确定是否正确/不正确:

重要说明:以下语句均假定操作是在同步块内完成的。

  • 消除ArrayList 的迭代过程中,应该使用 Iterator 来完成,因为如果在集合中间进行删除,for 循环会导致不可预知的行为。例子:
Iterator<Item> itemIterator = items.iterator();
while (itemIterator.hasNext()) {
  Item item = itemIterator.next();
  // check if item needs to be removed
  itemIterator.remove();
}
  • 对于添加操作,不能用 Iterator 完成,但可以用 ListIterator 完成。例子:
ListIterator<Item> itemIterator = list.listIterator();
while(itemIterator.hasNext()){
  \\ do some operation which requires iteration of the ArrayList
  itemIterator.add(item);                
}
  • 对于添加操作时,不一定要使用 ListIterator(即简单地使用 items.add(item) 不会导致任何问题)。
  • 对于添加遍历集合时的操作可以使用 ListIterator 或 for 循环来完成,但不能使用 Iterator。例子:
Iterator<Item> itemIterator = item.iterator();
while (itemIterator.hasNext()) {
  \\ do some operation which requires iteration of the ArrayList
  items.add(item); \\ NOT acceptable - cannot modify ArrayList while in an Iterator of that ArrayList
}
  • 修改ArrayList 中的项目可以使用 Iterator 或具有相同性能复杂度的 for 循环来完成(这是真的?).例子:
\\ iterator example

Iterator<Item> itemIterator = item.iterator();
while (itemIterator.hasNext()) {
  Item item = itemIterator.next();
  item.update(); // modifies the item within the ArrayList during iteration
}

\\ for loop example

for (Item item : items){
  item.update();
}

Iterator 迭代期间的修改是否具有与 for 循环相同的性能?这些方法之间是否存在任何线程安全差异?

奖金问题:如果还需要同步块,使用 ArrayList 的 synchronizedList 进行添加/删除/修改操作与 for 循环与迭代器相比有什么优势?

【问题讨论】:

  • for 循环有效地创建和使用Iterator。如果您允许在循环进行时修改列表,您将得到相同的异常。

标签: java performance arraylist concurrency thread-safety


【解决方案1】:

while循环和for循环之间没有区别,事实上,显式使用迭代器的循环的惯用形式是for循环:

for(Iterator<Item> it = items.iterator(); it.hasNext(); ) {
    Item item = it.next();
    item.update();
}

它被编译成与

for(Item item: items) {
  item.update();
}

Try it online!

对于依赖于用于生成它的原始源代码的相同编译代码,没有性能差异。


在插入或删除 ArrayList 的元素时,您不必关注循环形式,而必须关注基本限制。每次插入或删除元素时,受影响索引后面的元素都必须复制到新位置。这不是很昂贵,因为阵列只包含参考对对象,但重复做时成本很容易加起来。

因此,如果您知道插入或删除的数量可预见地很小,或者将在末尾或接近末尾发生(因此只有少量元素要复制),这不是问题。但是当在循环中的任意位置插入或删除任意数量的元素时,您会遇到二次时间复杂度.

您可以通过使用来避免这种情况

items.removeIf(item -> /* check and return whether to remove the item*/);

这将使用内部迭代并推迟元素的移动,直到它们的最终位置已知,从而导致线性时间复杂度.

如果那不可行,您最好将列表复制到新列表中,跳过不需要的元素。这会稍微降低效率,但仍然具有线性时间复杂度。这也是在任意位置插入大量项目的解决方案。


item.update(); 属于完全不同的类别。 “ArrayList 中的项目”是一种错误的思维方式。如上所述,ArrayList 包含参考对象,而对象本身不受“在 ArrayList 内部”的影响。事实上,对象可以同时存在于多个集合中,因为所有标准集合只包含引用。

所以item.update();改变了Item对象,这是一个独立于ArrayList的操作,当你假设基于列表的线程安全时,这是危险的。

当你有像这样的代码时

Item item = items.get(someIndex);
// followed by using item

其中 get 来自 synchronizedList

或将项目返回给调用者的手动同步检索操作或使用检索到的Item的任何其他形式的代码外部synchronized块,

然后你的代码不是线程安全的.当 update() 调用在同步或锁定下完成时在列表上循环时,当其他用途在同步或锁定之外时,它无济于事。为了线程安全,对象的所有使用都必须由相同的线程安全结构保护。


因此,即使您使用 synchronizedList,您也必须不仅手动保护您的循环,as the documentation already tells,您还必须将保护扩展到所包含元素的所有其他用途(如果它们是可变的)。

或者,如果您知道自己在做什么,您可以对列表和包含的元素使用不同的机制,但这仍然意味着“只用 synchronizedList 包装列表”的简单性并不存在。

那么它有什么优势呢?其实没有。它可能在从 Java 1.1 及其完全同步的VectorHashtable 迁移到 Java 2 的 Collection API 的过程中帮助了开发人员。但是我根本没有用过同步包装器。无论如何,任何重要的用例都需要手动同步(或锁定)。

【讨论】:

    猜你喜欢
    • 2023-03-27
    • 1970-01-01
    • 1970-01-01
    • 2018-05-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多