关于java:Guava MultiMap和ConcurrentModificationException

Guava MultiMap and ConcurrentModificationException

本问题已经有最佳答案,请猛点这里访问。

我不明白为什么当我迭代这个multimap时会得到一个并发修改例外。我读了下面的条目,但我不确定我是否完全理解。我试图添加一个同步块。但我的疑问是什么时候和什么时候同步。

multimap是一个字段,创建如下:

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

使用方法如下:

1
eventMultiMap.put(event, command);

像这样(我试着在地图上同步这个部分,但没有成功)

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


在迭代集合时对其调用Remove将导致每次都发生ConcurrentModificationException,即使所有操作都在同一线程中完成-正确的做法是获取显式迭代器并对此调用.Remove()。

编辑:修改示例:

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


您可能希望看到这个blogpost来寻找另一个陷阱,在遍历多映射时产生一个ConcurrentModificationException,而没有其他线程干扰。简而言之,如果您遍历multimap的键,访问与每个键相关联的值的各个集合,并从此类集合中删除一些元素,如果该元素恰好是集合的最后一个元素,则在尝试访问下一个键时,您将拥有ConcurrentModificationException,因为清空集合会触发删除e键,从而在结构上修改多映射的键集。


如果另一个线程可以在运行此逻辑时修改您的多映射,则需要向mharris的代码添加一个同步块:

1
2
3
4
5
6
7
8
9
synchronized (eventMultimap) {
  Iterator<Entry<GenericEvent, Command>> i = eventMultiMap.entries.iterator();
  while (i.hasNext()) {
    if (i.next().getValue().equals(command)) {
        i.remove();
        nbRemoved++;
    }
  }
}

或者,可以省略下面的迭代器,

1
2
3
4
5
synchronized (eventMultimap) {
  int oldSize = eventMultimap.size();
  eventMultimap.values().removeAll(Collections.singleton(command));
  nbRemoved = oldSize - eventMultimap.size();
}

removeall()调用不需要同步。但是,如果省略synchronized块,则多映射可能在removeall()调用和其中一个size()调用之间发生变化,从而导致nbremoved的值不正确。

现在,如果您的代码是单线程的,并且您只想避免一个ConcurrentModificationException调用,那么您可以省去multimaps.synchronizedMultimap和synchronized(eventmulimap)逻辑。


在Java8中,还可以使用lambda方法:

eventMultiMap.entries().removeIf(genericEventCommandEntry -> genericEventCommandEntry.getValue().equals(command));


如果你不在乎钥匙的话,我更喜欢Multimap.values().iterator()。您还应该尽量避免使用同步块,因为您不能有效地区分读/写的优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
  }
}