Sta iterando i valori di ConcurrentHashMap thread safe?

In javadoc per ConcurrentHashMap è il seguente:

Le operazioni di recupero (incluso get) generalmente non si bloccano, quindi potrebbero sovrapporsi alle operazioni di aggiornamento (incluse le operazioni di inserimento e rimozione). Recuperati riflettono i risultati delle operazioni di aggiornamento completate più recentemente che mantengono il loro esordio. Per operazioni di aggregazione come putAll e clear, i retrieval concorrenti possono riflettere l’inserimento o la rimozione di solo alcune voci. Allo stesso modo, Iterators ed Enumerations restituiscono elementi che riflettono lo stato della tabella hash a un certo punto o dalla creazione dell’iteratore / enumerazione. Non generano ConcurrentModificationException. Tuttavia, gli iteratori sono progettati per essere utilizzati da un solo thread alla volta.

Cosa significa? Cosa succede se provo ad iterare la mappa con due thread contemporaneamente? Cosa succede se inserisco o rimuovo un valore dalla mappa mentre lo ito?

Cosa significa?

Ciò significa che ogni iteratore ottenuto da una ConcurrentHashMap è progettato per essere utilizzato da un singolo thread e non deve essere distribuito. Ciò include lo zucchero sintattico fornito dal ciclo for-each.

Cosa succede se provo ad iterare la mappa con due thread contemporaneamente?

Funzionerà come previsto se ognuno dei thread utilizza il proprio iteratore.

Cosa succede se inserisco o rimuovo un valore dalla mappa mentre lo ito?

È garantito che le cose non si romperanno se lo fai (fa parte di ciò che significa “concurrent” in ConcurrentHashMap ). Tuttavia, non vi è alcuna garanzia che un thread vedrà le modifiche alla mappa eseguite dall’altro thread (senza ottenere un nuovo iteratore dalla mappa). L’iteratore è garantito per riflettere lo stato della mappa al momento della sua creazione. Futuri cambiamenti possono riflettersi nell’iteratore, ma non devono esserlo.

In conclusione, una dichiarazione come

 for (Object o : someConcurrentHashMap.entrySet()) { // ... } 

andrà bene (o almeno sicuro) quasi ogni volta che lo vedi.

È ansible utilizzare questa class per testare due thread di accesso e uno che modifica l’istanza condivisa di ConcurrentHashMap :

 import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ConcurrentMapIteration { private final Map map = new ConcurrentHashMap(); private final static int MAP_SIZE = 100000; public static void main(String[] args) { new ConcurrentMapIteration().run(); } public ConcurrentMapIteration() { for (int i = 0; i < MAP_SIZE; i++) { map.put("key" + i, UUID.randomUUID().toString()); } } private final ExecutorService executor = Executors.newCachedThreadPool(); private final class Accessor implements Runnable { private final Map map; public Accessor(Map map) { this.map = map; } @Override public void run() { for (Map.Entry entry : this.map.entrySet()) { System.out.println( Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']' ); } } } private final class Mutator implements Runnable { private final Map map; private final Random random = new Random(); public Mutator(Map map) { this.map = map; } @Override public void run() { for (int i = 0; i < 100; i++) { this.map.remove("key" + random.nextInt(MAP_SIZE)); this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString()); System.out.println(Thread.currentThread().getName() + ": " + i); } } } private void run() { Accessor a1 = new Accessor(this.map); Accessor a2 = new Accessor(this.map); Mutator m = new Mutator(this.map); executor.execute(a1); executor.execute(m); executor.execute(a2); } } 

Nessuna eccezione verrà lanciata.

La condivisione dello stesso iteratore tra i thread di accesso può portare a deadlock:

 import java.util.Iterator; import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ConcurrentMapIteration { private final Map map = new ConcurrentHashMap(); private final Iterator> iterator; private final static int MAP_SIZE = 100000; public static void main(String[] args) { new ConcurrentMapIteration().run(); } public ConcurrentMapIteration() { for (int i = 0; i < MAP_SIZE; i++) { map.put("key" + i, UUID.randomUUID().toString()); } this.iterator = this.map.entrySet().iterator(); } private final ExecutorService executor = Executors.newCachedThreadPool(); private final class Accessor implements Runnable { private final Iterator> iterator; public Accessor(Iterator> iterator) { this.iterator = iterator; } @Override public void run() { while(iterator.hasNext()) { Map.Entry entry = iterator.next(); try { String st = Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']'; } catch (Exception e) { e.printStackTrace(); } } } } private final class Mutator implements Runnable { private final Map map; private final Random random = new Random(); public Mutator(Map map) { this.map = map; } @Override public void run() { for (int i = 0; i < 100; i++) { this.map.remove("key" + random.nextInt(MAP_SIZE)); this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString()); } } } private void run() { Accessor a1 = new Accessor(this.iterator); Accessor a2 = new Accessor(this.iterator); Mutator m = new Mutator(this.map); executor.execute(a1); executor.execute(m); executor.execute(a2); } } 

Non appena inizi a condividere lo stesso Iterator

>

tra i thread accessor e mutator java.lang.IllegalStateException s inizierà a comparire.

 import java.util.Iterator; import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ConcurrentMapIteration { private final Map map = new ConcurrentHashMap(); private final Iterator> iterator; private final static int MAP_SIZE = 100000; public static void main(String[] args) { new ConcurrentMapIteration().run(); } public ConcurrentMapIteration() { for (int i = 0; i < MAP_SIZE; i++) { map.put("key" + i, UUID.randomUUID().toString()); } this.iterator = this.map.entrySet().iterator(); } private final ExecutorService executor = Executors.newCachedThreadPool(); private final class Accessor implements Runnable { private final Iterator> iterator; public Accessor(Iterator> iterator) { this.iterator = iterator; } @Override public void run() { while (iterator.hasNext()) { Map.Entry entry = iterator.next(); try { String st = Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']'; } catch (Exception e) { e.printStackTrace(); } } } } private final class Mutator implements Runnable { private final Random random = new Random(); private final Iterator> iterator; private final Map map; public Mutator(Map map, Iterator> iterator) { this.map = map; this.iterator = iterator; } @Override public void run() { while (iterator.hasNext()) { try { iterator.remove(); this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString()); } catch (Exception ex) { ex.printStackTrace(); } } } } private void run() { Accessor a1 = new Accessor(this.iterator); Accessor a2 = new Accessor(this.iterator); Mutator m = new Mutator(map, this.iterator); executor.execute(a1); executor.execute(m); executor.execute(a2); } } 

Significa che non si dovrebbe condividere un object iteratore tra più thread. La creazione di più iteratori e il loro utilizzo simultaneo in thread separati va bene.

Questo potrebbe darti una buona intuizione

ConcurrentHashMap raggiunge una maggiore concorrenza riducendo leggermente le promesse fatte ai chiamanti. Un’operazione di recupero restituirà il valore inserito dall’operazione di inserimento completata più recente e può anche restituire un valore aggiunto da un’operazione di inserimento che è contemporaneamente in corso (ma in nessun caso restituirà un risultato senza senso). Iterator restituiti da ConcurrentHashMap.iterator () restituiranno ogni elemento una volta al massimo e non genereranno mai ConcurrentModificationException, ma potrebbero o meno riflettere gli inserimenti o le rimozioni avvenute da quando è stato creato l’iteratore . Non è necessario alcun blocco a livello di tabella (o anche ansible) per garantire la sicurezza del thread durante l’iterazione della raccolta. ConcurrentHashMap può essere utilizzato come sostituto di synchronizedMap o Hashtable in qualsiasi applicazione che non si basa sulla possibilità di bloccare l’intera tabella per evitare aggiornamenti.

A proposito di questo:

Tuttavia, gli iteratori sono progettati per essere utilizzati da un solo thread alla volta.

Significa che, mentre gli iteratori prodotti da ConcurrentHashMap in due thread sono sicuri, potrebbe causare un risultato imprevisto nell’applicazione.

Cosa significa?

Significa che non dovresti provare a usare lo stesso iteratore in due thread. Se hai due thread che devono scorrere le chiavi, i valori o le voci, allora ognuno dovrebbe creare e usare i propri iteratori.

Cosa succede se provo ad iterare la mappa con due thread contemporaneamente?

Non è del tutto chiaro cosa succederebbe se tu rompessi questa regola. Si potrebbe semplicemente ottenere un comportamento confuso, nello stesso modo in cui lo si fa se (ad esempio) due thread cercano di leggere dallo standard input senza sincronizzarsi. Si potrebbe anche ottenere un comportamento non thread-safe.

Ma se i due thread usassero iteratori diversi, dovresti stare bene.

Cosa succede se inserisco o rimuovo un valore dalla mappa mentre lo ito?

Questo è un problema separato, ma la sezione javadoc che hai citato risponde adeguatamente. Fondamentalmente, gli iteratori sono thread-safe, ma non è definito se vedrete gli effetti di eventuali inserimenti, aggiornamenti o eliminazioni simultanee riflesse nella sequenza di oggetti restituiti dall’iteratore. In pratica, probabilmente dipende da dove nella mappa si verificano gli aggiornamenti.