如何在并发线程中操作`values()`和`put()`时避免使用HashMap“ConcurrentModificationException”?

码:

我有一个HashMap

private Map map = new HashMap(); 

一种方法是通过调用put(K,V)将KV对放入其中。

另一种方法想从其值中提取一组随机元素:

 int size = map.size(); // size > 0 V[] value_array = map.values().toArray(new V[size]); Random rand = new Random(); int start = rand.nextInt(size); int end = rand.nextInt(size); // return value_array[start .. end - 1] 

这两个方法在两个不同的并发线程中调用。


错误:

我收到了一个ConcurrentModificationException错误:

 at java.util.HashMap$HashIterator.nextEntry(Unknown Source) at java.util.HashMap$ValueIterator.next(Unknown Source) at java.util.AbstractCollection.toArray(Unknown Source) 

似乎一个线程中的toArray()方法实际上是在HashMap上迭代,并且在其他线程中发生了put()修改。

问题:如何在并发线程中使用HashMap.values()。toArray()和HashMap.put()时避免“ConcurrentModificationException”?
直接避免在第二种方法中使用values().toArray()也行。

您需要提供某种级别的同步,以便在执行toArray调用时阻止对put的调用,反之亦然。 有两种简单的方法:

  1. 将对puttoArray调用包装在synchronized块中,这些块在同一个锁对象(可能是映射本身或其他对象)上进行同步。
  2. 使用Collections.synchronizedMap()将地图转换为同步地图

     private Map map = Collections.synchronizedMap(new HashMap<>()); 

  3. 使用ConcurrentHashMap而不是HashMap

编辑:使用Collections.synchronizedMap的问题是,一旦调用values()返回,并发保护将消失。 此时,对put()toArray()调用可能会同时执行。 ConcurrentHashMap有一些类似的问题,但它仍然可以使用。 从ConcurrentHashMap.values()的文档:

视图的迭代器是一个“弱一致”的迭代器,它永远不会抛出ConcurrentModificationException ,并保证遍历构造迭代器时存在的元素,并且可能(但不保证)反映构造之后的任何修改。

我会使用ConcurrentHashMap而不是HashMap,并保护它免受不同线程的并发读取和修改。 请参阅以下实施。 线程1和线程2不可能同时读写。 当线程1将值从Map提取到数组时,调用storeInMap(K,V)的所有其他线程将挂起并在地图上等待,直到第一个线程完成对象。

注意:我不在此上下文中使用synchronized方法; 我不完全排除同步方法,但我会谨慎使用它。 同步方法实际上只是用于获取“this”锁定并在方法持续时间内保持锁定的语法糖,因此它可能会损害吞吐量。

 private Map map = new ConcurrentHashMap(); // thread 1 public V[] pickRandom() { int size = map.size(); // size > 0 synchronized(map) { V[] value_array = map.values().toArray(new V[size]); } Random rand = new Random(); int start = rand.nextInt(size); int end = rand.nextInt(size); return value_array[start .. end - 1] } // thread 2 public void storeInMap(K, V) { synchronized(map) { map.put(K,V); } }