5

Codice:Come evitare HashMap "ConcurrentModificationException" mentre si manipolano `values ​​()` e `put()` in thread simultanei?

Ho un HashMap

private Map<K, V> map = new HashMap<>(); 

Un metodo metterà coppia K-V in esso chiamando put(K,V).

L'altro metodo vuole estrarre un insieme di elementi casuali dai suoi valori:

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] 

I due metodi vengono chiamati in due differenti thread concorrenti.


Errore:

ho ottenuto un errore di ConcurrentModificationException:

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

Sembra che il metodo toArray() in un thread è in realtà scorrere il HashMap e una modifica put() in altro thread si verifica.

Question: How to avoid "ConcurrentModificationException" while using HashMap.values().toArray() and HashMap.put() in concurrent threads?
Directly avoiding using values().toArray() in the second method is also OK.

+1

eseguire il codice che accede al 'map' in un blocco di sincronizzazione:' sincronizzazione (mappa) {...} '' – Titus

+1

sincronizzazione (mappa) {..} 'dovrebbe funzionare (se lo si applica in tutto il mondo). Collections.synchronizedMap non funzionerà. vedere http://docs.oracle.com/javase/7/docs/api/java/util/Collections.html#synchronizedMap%28java.util.Map%29 – Thilo

risposta

4

è necessario fornire un certo livello di sincronizzazione in modo che la chiamata è bloccata put mentre la chiamata toArray sta eseguendo e viceversa. Ci sono tre due approcci semplici:

  1. avvolgere le chiamate ad put e toArray in synchronized blocchi che sincronizzano sul medesimo oggetto di blocco (che potrebbe essere la mappa stessa o qualche altro oggetto).
  2. Accendere la mappa in una cartina sincronizzato utilizzando Collections.synchronizedMap()

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

  3. Utilizzare un ConcurrentHashMap invece di un HashMap.

EDIT: Il problema con l'utilizzo Collections.synchronizedMap è che una volta che la chiamata a values() rendimenti, la tutela della concorrenza scomparirà. A quel punto, le chiamate a put() e toArray() potrebbero essere eseguite contemporaneamente. A ConcurrentHashMap ha un problema simile, ma può ancora essere utilizzato. Dalla documentazione per 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.

+0

@Thilo - A destra. Tre approcci. :) –

+0

@Thilo Grazie. Tuttavia, ho letto alcune osservazioni sul fatto che ConcurrentHashMap non risolve necessariamente ConcurrentModificationException (ma non riesce a trovare la sorgente ora). Perché funziona in questo/mio caso? – hengxin

+1

È necessario 'values ​​()' per funzionare multi-threaded, e Javadoc dice "L'iteratore della vista è un iteratore" debolmente coerente "che non genererà ConcurrentModificationException e garantisce di attraversare gli elementi così come sono esistiti durante la costruzione dell'iteratore, e può (ma non è garantito) riflettere eventuali modifiche successive alla costruzione. " – Thilo

0

userei ConcurrentHashMap invece di una HashMap e proteggerlo da lettura simultanea e la modifica da diversi thread. Vedere l'implementazione di seguito. Non è possibile che thread 1 e thread 2 leggano e scrivano allo stesso tempo. Quando il thread 1 estrae i valori da Map a un array, tutti gli altri thread che invocano storeInMap (K, V) sospenderanno e attenderanno sulla mappa fino a quando il primo thread non avrà terminato con l'oggetto.

Nota: non uso il metodo sincronizzato in questo contesto; Non escludo completamente il metodo sincronizzato, ma lo userei con cautela. Un metodo sincronizzato è in realtà solo lo zucchero di sintassi per ottenere il blocco su "this" e tenerlo premuto per tutta la durata del metodo in modo che possa danneggiare il 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); 
    } 
} 
+1

Perché i blocchi 'sincronizzati' se stai già utilizzando' ConcurrentHashMap'? –

Problemi correlati