Code:
I have a HashMap
private Map<K, V> map = new HashMap<>();
One method will put K-V pair into it by calling put(K,V)
.
The other method wants to extract a set of random elements from its values:
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]
The two methods are called in two different concurrent threads.
Error:
I got a ConcurrentModificationException
error:
at java.util.HashMap$HashIterator.nextEntry(Unknown Source)
at java.util.HashMap$ValueIterator.next(Unknown Source)
at java.util.AbstractCollection.toArray(Unknown Source)
It seems that the toArray()
method in one thread is actually iterating over the HashMap and a put()
modification in other thread occurs.
Question: How to avoid "ConcurrentModificationException" while using HashMap.values().toArray() and HashMap.put() in concurrent threads?
Directly avoiding usingvalues().toArray()
in the second method is also OK.
ConcurrentHashMap does not throw ConcurrentModificationException if the underlying collection is modified during an iteration is in progress. Iterators may not reflect the exact state of the collection if it is being modified concurrently.
How do you fix Java's ConcurrentModificationException? There are two basic approaches: Do not make any changes to a collection while an Iterator loops through it. If you can't stop the underlying collection from being modified during iteration, create a clone of the target data structure and iterate through the clone.
The ConcurrentHashMap class allows multiple threads to access its entries concurrently. By default, the concurrent hashmap is divided into 16 segments. This is the reason why 16 threads are allowed to concurrently modify the map at the same time. However, any number of threads can access the map at a time.
You need to provide some level of synchronization so that the call to put
is blocked while the toArray
call is executing and vice versa. There are three two simple approaches:
put
and toArray
in synchronized
blocks that synchronize on the same lock object (which might be the map itself or some other object).Turn your map into a synchronized map using Collections.synchronizedMap()
private Map<K, V> map = Collections.synchronizedMap(new HashMap<>());
Use a ConcurrentHashMap
instead of a HashMap
.
EDIT: The problem with using Collections.synchronizedMap
is that once the call to values()
returns, the concurrency protection will disappear. At that point, calls to put()
and toArray()
might execute concurrently. A ConcurrentHashMap
has a somewhat similar problem, but it can still be used. From the docs for ConcurrentHashMap.values()
:
The view's iterator is a "weakly consistent" iterator that will never throw
ConcurrentModificationException
, and guarantees to traverse elements as they existed upon construction of the iterator, and may (but is not guaranteed to) reflect any modifications subsequent to construction.
I would use ConcurrentHashMap instead of a HashMap and protect it from concurrent reading and modification by different threads. See the below implementation. It is not possible for thread 1 and thread 2 to read and write at the same time. When thread 1 is extracting values from Map to an array, all other threads that invoke storeInMap(K, V) will suspend and wait on the map until the first thread is done with the object.
Note: I do not use synchronized method in this context; I do not completely rule out synchronized method but I would use it with caution. A synchronized method is actually just syntax sugar for getting the lock on 'this' and holding it for the duration of the method so it can hurt throughput.
private Map<K, V> map = new ConcurrentHashMap<K, V>();
// 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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With