113

In javadoc per ConcurrentHashMap è il seguente:Is iterating ConcurrentHashMap valori thread sicuro?

operazioni di recupero (compresi get) generalmente non ostruire, così può sovrapporsi con le operazioni di aggiornamento (comprese mettere e togliere). 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.

Che 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?

risposta

147

Che cosa significa?

Ciò significa che ciascun iteratore ottenuto da un ConcurrentHashMap è progettato per essere utilizzato da un singolo thread e non deve essere trasferito. 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 itera?

È garantito che le cose non si interrompano se si esegue questa operazione (questo è parte di ciò che il "concurrent" in ConcurrentHashMap significa). 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 di sicurezza) quasi ogni volta che lo vedi.

+0

Quindi cosa succede se durante l'iterazione, un altro thread ha rimosso un oggetto o10 ​​dalla mappa? Posso ancora vedere o10 nell'iterazione anche se è stato rimosso? @Waldheinz – Alex

+0

Come detto sopra, non è realmente specificato se un iteratore esistente rifletterà le modifiche successive alla mappa. Quindi non lo so, e per specifica nessuno lo fa (senza guardare il codice, e questo può cambiare ad ogni aggiornamento del runtime). Quindi non puoi fare affidamento su di esso. – Waldheinz

+4

Ma ho ancora ottenuto un 'ConcurrentModificationException' mentre iterando una' ConcurrentHashMap', perché? –

5

This potrebbe darvi una buona intuizione

ConcurrentHashMap raggiunge maggiore concorrenza leggermente rilassante le promesse fa per i 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() restituiscono ogni elemento una volta al massimo e non generano mai ConcurrentModificationException, ma possono o potrebbero non riflettere inserimenti o rimozioni avvenute da quando è stato creato l'iteratore. Non è necessario alcun blocco a livello di tabella (o anche possibile) 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.

questo proposito:

Tuttavia, iteratori sono progettati per essere utilizzati da un solo filo per volta.

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

8

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

+0

Qualche motivo per cui non si è capitalizzato l'I in Iterator?Dal momento che è il nome della classe, potrebbe essere meno confuso. –

+1

@ Bill Michell, ora siamo nella semantica del posting galateo. Penso che avrebbe dovuto rendere a Iterator un link a javadoc per un Iterator, o perlomeno inserirlo nelle annotazioni del codice inline ('). –

17

È possibile utilizzare questa classe per testare due thread di accesso e una mutazione 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<String, String> map = new ConcurrentHashMap<String, String>(); 

    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<String, String> map; 

    public Accessor(Map<String, String> map) 
    { 
     this.map = map; 
    } 

    @Override 
    public void run() 
    { 
     for (Map.Entry<String, String> entry : this.map.entrySet()) 
     { 
     System.out.println(
      Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']' 
     ); 
     } 
    } 
    } 

    private final class Mutator implements Runnable 
    { 

    private final Map<String, String> map; 
    private final Random random = new Random(); 

    public Mutator(Map<String, String> 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); 
    } 
} 

verrà generata Nessuna eccezione.

Condividendo lo stesso iteratore tra i thread accessor può portare a situazione di stallo:

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<String, String> map = new ConcurrentHashMap<String, String>(); 
    private final Iterator<Map.Entry<String, String>> 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<Map.Entry<String, String>> iterator; 

    public Accessor(Iterator<Map.Entry<String, String>> iterator) 
    { 
     this.iterator = iterator; 
    } 

    @Override 
    public void run() 
    { 
     while(iterator.hasNext()) { 
     Map.Entry<String, String> 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<String, String> map; 
    private final Random random = new Random(); 

    public Mutator(Map<String, String> 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 si inizia a condividere la stessa Iterator<Map.Entry<String, String>> tra accessor e mutator discussioni java.lang.IllegalStateException s inizierà popping up.

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<String, String> map = new ConcurrentHashMap<String, String>(); 
    private final Iterator<Map.Entry<String, String>> 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<Map.Entry<String, String>> iterator; 

    public Accessor(Iterator<Map.Entry<String, String>> iterator) 
    { 
     this.iterator = iterator; 
    } 

    @Override 
    public void run() 
    { 
     while (iterator.hasNext()) 
     { 
     Map.Entry<String, String> 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<Map.Entry<String, String>> iterator; 

    private final Map<String, String> map; 

    public Mutator(Map<String, String> map, Iterator<Map.Entry<String, String>> 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); 
    } 
} 
+0

Sei sicuro che "Condivisione dello stesso iteratore tra thread di accesso può portare a deadlock"? Il documento dice che read non è bloccato e ho provato il tuo programma e non si verifica ancora nessun deadlock. Anche se il risultato iterato sarà sbagliato. – Tony

4

Che cosa significa?

Significa che non si deve provare a utilizzare 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 si rompesse 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 utilizzano iteratori diversi, si dovrebbe andare bene.

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

Questo è un problema separato, ma la sezione javadoc che hai citato risponde adeguatamente. Fondamentalmente, gli iteratori sono thread-safe, ma non è definito se si vedranno 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.