Il thread iterante dei valori ConcurrentHashMap è sicuro?


156

In javadoc per ConcurrentHashMap è il seguente:

Le operazioni di recupero (incluso get) generalmente non si bloccano, pertanto potrebbero sovrapporsi con le operazioni di aggiornamento (incluso put e remove). I recuperi riflettono i risultati delle operazioni di aggiornamento completate più di recente che si sono manifestate al loro inizio. Per operazioni aggregate come putAll e clear, i recuperi simultanei possono riflettere l'inserimento o la rimozione di solo alcune voci. Allo stesso modo, Iteratori ed Enumerazioni restituiscono elementi che riflettono lo stato della tabella hash ad un certo punto in 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 a ripetere la mappa con due thread contemporaneamente? Cosa succede se inserisco o rimuovo un valore dalla mappa durante l'iterazione?

Risposte:


193

Cosa significa?

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

Cosa succede se provo a ripetere la mappa con due thread contemporaneamente?

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

Cosa succede se inserisco o rimuovo un valore dalla mappa durante l'iterazione?

È garantito che le cose non si rompono se lo fai (fa parte di ciò che significa "concorrente" 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. Ulteriori cambiamenti possono essere riflessi nell'iteratore, ma non devono esserlo.

In conclusione, una dichiarazione simile

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

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


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

Come detto sopra, non è in realtà 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 con ogni aggiornamento del runtime). Quindi non puoi fare affidamento su di esso.
Waldheinz,

8
Ma ho ancora un ConcurrentModificationExceptionpo 'di iterazione di un ConcurrentHashMap, perché?
Kimi Chiu,

@KimiChiu dovresti probabilmente pubblicare una nuova domanda fornendo il codice che attiva quell'eccezione, ma dubito fortemente che derivi direttamente dall'iterazione di un contenitore concorrente. a meno che l'implementazione Java non sia corretta.
Waldheinz,

18

È possibile utilizzare questa classe per testare due thread di accesso e uno di mutazione dell'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);
  }
}

Non verrà generata alcuna eccezione.

La condivisione dello stesso iteratore tra i thread degli accessori 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<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 inizi a condividere lo stesso Iterator<Map.Entry<String, String>>tra i thread degli accessori e dei mutatori java.lang.IllegalStateExceptioninizieranno 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<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);
  }
}

Sei sicuro di "Condividere lo stesso iteratore tra thread di accessori può portare a deadlock"? Il documento dice che la lettura non è bloccata e ho provato il tuo programma e non si verificano ancora deadlock. Anche se il risultato ripetuto sarà sbagliato.
Tony,

12

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


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

1
@ Bill Michell, ora siamo nella semantica della pubblicazione dell'etichetta. Penso che avrebbe dovuto fare di Iterator un link al javadoc per un Iterator, o almeno inserirlo all'interno delle annotazioni del codice inline (`).
Tim Bender,

10

Questo potrebbe darti una buona visione

ConcurrentHashMap raggiunge una maggiore concorrenza allentando leggermente le promesse che fa ai chiamanti. Un'operazione di recupero restituirà il valore inserito dall'operazione di inserimento completata più recente e potrebbe anche restituire un valore aggiunto da un'operazione di inserimento che è contemporaneamente in corso (ma in nessun caso restituirà un risultato senza senso). Gli iter restituiti da ConcurrentHashMap.iterator () restituiranno ogni elemento al massimo una volta e non genereranno mai ConcurrentModificationException, ma potrebbero riflettere o meno inserimenti o rimozioni verificatisi dopo la creazione dell'iteratore. Nessun blocco a livello di tavolo è necessario (o addirittura 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 impedire gli aggiornamenti.

A riguardo:

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

Ciò significa che, sebbene l'utilizzo degli iteratori prodotti da ConcurrentHashMap in due thread sia sicuro, può causare un risultato imprevisto nell'applicazione.


4

Cosa significa?

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

Cosa succede se provo a ripetere la mappa con due thread contemporaneamente?

Non è del tutto chiaro cosa succederebbe se infrangessi questa regola. Potresti ottenere un comportamento confuso, allo stesso modo in cui fai (ad esempio) due thread tentano di leggere dall'input standard senza sincronizzarsi. È inoltre possibile ottenere comportamenti non thread-safe.

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

Cosa succede se inserisco o rimuovo un valore dalla mappa durante l'iterazione?

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.