列表抛出ConcurrentModificationException传递给CompletableFutures。

huangapple go评论67阅读模式
英文:

List throwing ConcurrentModificationException passed to CompletableFutures

问题

Scenario 1:::
因此,我有一个名为params的列表,它被传递给两个方法,这两个方法调用 web 服务并获取数据。这两个方法只是在params列表上执行stream.filter.collect操作,以获取调用其余参数所需的参数。
现在我使用CompletableFutures使这两个调用并行进行。
这会引发ConcurrentModification异常吗?

Scenario 2:::
与上述情况类似,只是现在一个方法会更改params列表,并向其中添加一些对象。我知道这会引发Concurrent Modification异常。我应该将列表设置为copyonWriteArrayList,或者创建一个带有深度复制的新列表,以避免进一步的问题。

英文:

Scenario 1:::
So I have list of params, which is passed to 2 methods which calls web service and gets the data. These to methods just do stream.filter.collect on the list of params to get the needed parameter for rest call.
Now I have made the 2 calls parallel using CompletableFutures.
Can this throw ConcurrentModifcation exception?

Scenario 2:::
Similar setup as above , just that now one method changes the list of params and adds some objects to it. I know this is throwing Concurrent Modification exp. Should I just make list as copyonWriteArraylist or create new list with deep copy to avoid any further problems.

答案1

得分: 0

场景#1:很可能不会,但是您的描述过于模糊,无法确定。

场景#2:绝对如此。

CoModEx发生的唯一条件是以任何方式更改列表。无论是addaddAllclearremoveretainAll还是List上的任何其他更改列表本身的方法。甚至获取子列表并更改该子列表(因为对子列表的更改从创建子列表的“外部”列表可见)。

尽管使用了“并发”一词,但CoModEx与线程无关。实际上,同时从两个线程操纵列表是破坏事物的几种方法之一(方法不再按照其javadoc所说的那样工作),而不会引发ConcurrentModificationException(这将取决于竞争条件的进行方式)。

以下是获得CoModEx的一个微不足道的方法:

var list = new ArrayList<String>();
list.add("Hello");
list.add("World");
for (String item : list) if (item.equals("Hello")) list.remove(item);

这将抛出异常。每次都会。当底层数据结构以任何不是由(spl)迭代器本身直接执行的方式进行更改时,CoModEx由__迭代器__(for (x:y)构造函数将隐式创建迭代器,x.stream()...也会创建一个分隔器,分隔器也会这样做)抛出。例如,这是使用迭代器从列表中移除项目的一种方式,而不会导致CoModEx:

var it = list.iterator();
while (it.hasNext()) {
    if (it.next().startsWith("Hello")) it.remove();
}

请注意,我调用的是迭代器的remove,而不是列表的remove,后者会导致CoModEx:它会更改底层列表(而不是通过迭代器直接更改),因此在修改之前创建的任何迭代器上的任何操作都会引发CoModEx。

因此,流程如下:

  1. 通过输入for (String item : list),您从list创建了一个迭代器。
  2. 调用该迭代器的hasNext()以检查是否应该进入for循环。它返回true
  3. 第一次循环调用了该迭代器的next();返回了Hello
  4. 由于for循环内的代码,调用了list.remove("Hello")。这使得到目前为止由此列表创建的所有迭代器“失效”。
  5. for循环循环,并调用hasNext()以检查是否应该再次循环。
  6. hasNext意识到它无效,并抛出CoModEx。

ArrayList通过具有每次更改时递增的计数器来实现此目的,并且所有迭代器都记住在创建时的计数器值,并检查列表的计数器值是否等于它们自己的值。如果不是,则它们会抛出CoModEx。如果需要,其他列表实现可以使用不同的机制。某些列表实现实际上允许这样做(例如CopyOnWriteArrayList,它明确详细说明了在迭代期间如何允许修改自身)。

如果涉及多个线程,则一切都未知 - 这些计数器写入未同步,因此可能或可能不会被涉及的线程看到。除非您确切知道自己在做什么,否则不要从不同的线程访问同一列表。

英文:

Scenario #1: Probably not, but your description is too vague to be sure.

Scenario #2: Most absolutely.

The only thing you need for CoModEx to occur is that the list is changed in any way. Be it add, addAll, clear, remove, retainAll, or any other method on List that has the effect of changing the list itself. Even fetching a sublist and changing THAT (as changes to sublist are visible from the 'outer' list that the sublist was created from).

CoModEx, despite the use of the word 'concurrent', has zip squat to do with threads. In fact, messing with a list from two threads simultaneously is one of the few ways you can break things (methods no longer do what their javadoc says they should) without causing a ConcurrentModificationException (will depend on how the race condition goes).

Here is a trivial way to get a CoModEx:

var list = new ArrayList&lt;String&gt;();
list.add(&quot;Hello&quot;);
list.add(&quot;World&quot;);
for (String item : list) if (item.equals(&quot;Hello&quot;)) list.remove(item);

That will throw it. Every time. CoModEx is thrown by iterators (and the for (x:y) constructor will implicitly create iterators, as does x.stream()..., which creates a spliterator, which also does this) when the underlying data structure was changed in any way that is not directly done by the (spl)iterator itself. For example, this is the one way you get to remove things from your own list using an iterator that does not result in CoModEx:

var it = list.iterator();
while (it.hasNext()) {
    if (it.next().startsWith(&quot;Hello&quot;)) it.remove();
}

Note I'm calling iterator's remove, not list's remove, which would have caused CoModEx: That would change the underlying list (and not via the iterator directly), therefore any operation on an iterator created before the modification will throw CoModEx.

So, this is the flow:

  1. You create an iterator from list, by entering for (String item : list).
  2. That iterator's hasNext() is invoked to check if the for loop should be entered. It returns true
  3. That iterator's next() is invoked for the first loop; Hello is returned.
  4. Due to the code inside the for loop, list.remove(&quot;Hello&quot;) is invoked. This 'invalidates' all iterators that were created by this list so far.
  5. the for loop loops, and invokes hasNext() to check if it should loop again.
  6. hasNext realizes that it is invalid, and throws CoModEx.

ArrayList does this by having a counter which is incremented every time anything changes, and all iterators remember the value of the counter when created, and check that the list's counter value is equal to their own. If not, they throw CoModEx. Other list impls can use different mechanisms if they desire. Some go out of their way to actually allow this (such as CopyOnWriteArrayList, which explicitly details how it DOES let you modify itself during iteration).

If multiple threads are involved, all bets are off - those counter writes are not synchronized and therefore may or may not be visible by the threads involved. Don't access the same list from different threads unless you really know what you are doing.

huangapple
  • 本文由 发表于 2020年9月20日 23:53:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/63980885.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定