Guava MultiMap 和 ConcurrentModificationException

10

我不明白为什么在遍历这个multimap时会出现ConcurrentModificationException异常。我阅读了下面的条目,但我不确定是否完全理解了其中的内容。 我尝试添加了一个同步块。但我的疑问是,要与何同步,以及何时同步。

multimap是一个字段,并像这样创建:

private Multimap<GenericEvent, Command> eventMultiMap =   
   Multimaps.synchronizedMultimap(HashMultimap.<GenericEvent, Command> create());

并且可以像这样使用:

eventMultiMap.put(event, command);

像这样(我试图将此部分与地图同步,但没有成功)

for (Entry<GenericEvent, Command> entry : eventMultiMap.entries()) {
    if (entry.getValue().equals(command)) {
        eventMultiMap.remove(entry.getKey(), entry.getValue());
        nbRemoved++;
    }
}

请参见https://dev59.com/T3I-5IYBdhLWcg3wxruQ。 - finnw
5个回答

11

在遍历一个集合时调用remove方法,即使在同一线程中执行,也会每次都导致ConcurrentModificationException异常-正确的做法是获取显式迭代器,然后调用其 .remove() 方法。

编辑:修改您的示例:

Iterator<Map.Entry<GenericEvent, Command>> i = eventMultiMap.entries().iterator();
while (i.hasNext()) {
    if (i.next().getValue().equals(command)) {
        i.remove();
        nbRemoved++;
    }
}

好的,我明白了。但是Iterator<E>只能使用一个参数类型。对吧?如果我遍历我的map中的值,并删除其中一个值。那么在原始map(eventMultiMap)中相应的值也会被删除吗? - Antoine Claval
好的,映射值上的迭代器可以删除值。谢谢。我现在不能接受您的回复,Iterator<E,T>无法编译,但请编辑它,我会接受。 - Antoine Claval
抱歉,我在工作中没有google-collections,所以在发布代码之前无法进行测试。 如果它们的设计类似于Java HashMap,则应该可以使用Entry对象上的迭代器 - 我已经编辑过了,并将在回家后再次检查,以防您在此期间没有尝试过它。 - MHarris

5

你可能希望查看这篇博客文章,了解另一种在遍历多重映射时产生ConcurrentModificationException的陷阱,而没有其他线程干扰。简而言之,如果你遍历多重映射的键,访问与每个键相关联的值的相应集合,并从这样的集合中删除某个元素,如果该元素恰好是集合的最后一个元素,则在尝试访问下一个键时会出现ConcurrentModificationException - 因为空集触发了键的删除,从而结构上修改了多重映射的键集。


因此,解决此问题的方法是在迭代时检查值集合的大小,如果在要从中删除时该值集合只剩下一个条目,则只需从键集的迭代器中删除。 - Alkanshel

4
如果在此代码逻辑运行期间其他线程可以修改您的multimap,则需要向MHarris的代码添加同步块:
synchronized (eventMultimap) {
  Iterator<Entry<GenericEvent, Command>> i = eventMultiMap.entries.iterator();
  while (i.hasNext()) {
    if (i.next().getValue().equals(command)) {
        i.remove();
        nbRemoved++;
    }
  }
}

或者,您可以按以下方式省略迭代器:
synchronized (eventMultimap) {
  int oldSize = eventMultimap.size();
  eventMultimap.values().removeAll(Collections.singleton(command));
  nbRemoved = oldSize - eventMultimap.size();
}

调用removeAll()方法不需要同步。但是,如果省略同步块,则multimap可能会在removeAll()调用和size()调用之间发生变化,导致nbRemoved值不正确。

现在,如果您的代码是单线程的,只想避免ConcurrentModificationException调用,您可以省略Multimaps.synchronizedMultimap和synchronized(eventMultimap)逻辑。


我喜欢使用removeAll(Collection.singleton(stuffToRemove))。谢谢。 - Antoine Claval

1

如果您不关心键,我建议使用Multimap.values().iterator()。您还应尽可能避免使用同步块,因为无法有效地设置读/写优先级。

ReadWriteLock lock = new ReentrantReadWriteLock();
Lock writeLock = lock.writeLock(); 

public void removeCommands(Command value) {
  try {
    writeLock.lock();
    for (Iterator<Command> it = multiMap.values().iterator(); it.hasNext();) {
      if (it.next() == value) {
        it.remove();
      }
    }
  } finally {
    writeLock.unlock();
  }
}

1
在Java8中,您还可以使用lambda方法:
``` eventMultiMap.entries().removeIf(genericEventCommandEntry -> genericEventCommandEntry.getValue().equals(command)); ```

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接