Blocchi sincronizzati Java e Collections.synchronizedMap


85

Il codice seguente è impostato per sincronizzare correttamente le chiamate synchronizedMap?

public class MyClass {
  private static Map<String, List<String>> synchronizedMap = Collections.synchronizedMap(new HashMap<String, List<String>>());

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      //do something with values
    }
  }

  public static void addToMap(String key, String value) {
    synchronized (synchronizedMap) {
      if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
      }
      else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
      }
    }
  }
}

Dalla mia comprensione, ho bisogno del blocco sincronizzato addToMap()per impedire a un altro thread di chiamare remove()o containsKey()prima di passare la chiamata a, put()ma non ho bisogno di un blocco sincronizzato doWork()perché un altro thread non può entrare nel blocco sincronizzato addToMap()prima del remove()ritorno perché ho creato la mappa originariamente con Collections.synchronizedMap(). È corretto? C'è un modo migliore per farlo?

Risposte:


90

Collections.synchronizedMap() garantisce che ogni operazione atomica che si desidera eseguire sulla mappa verrà sincronizzata.

L'esecuzione di due (o più) operazioni sulla mappa, tuttavia, deve essere sincronizzata in un blocco. Quindi sì, stai sincronizzando correttamente.


26
Penso che sarebbe bene menzionare che questo funziona perché i javadoc affermano esplicitamente che synchronizedMap si sincronizza sulla mappa stessa e non un blocco interno. Se fosse così, la sincronizzazione (synchronizedMap) non sarebbe corretta.
extraneon

2
@ Yuval potresti spiegare la tua risposta in modo un po 'più approfondito? Dici che sychronizedMap esegue le operazioni in modo atomico, ma allora perché avresti mai bisogno del tuo blocco sincronizzato se syncMap rendesse atomiche tutte le tue operazioni? Il tuo primo paragrafo sembra precludere la preoccupazione per il secondo.
almel

@almel vedi la mia risposta
Sergey

2
perchè è necessario avere il blocco sincronizzato visto che la mappa già utilizza Collections.synchronizedMap()? Non ho capito il secondo punto.
Bimal Sharma


13

C'è il potenziale per un bug sottile nel tuo codice.

[ AGGIORNAMENTO: Dato che sta usando map.remove () questa descrizione non è del tutto valida. Mi è mancato quel fatto la prima volta. :( Grazie all'autore della domanda per averlo sottolineato. Lascio il resto così com'è, ma ho cambiato la dichiarazione principale per dire che c'è potenzialmente un bug.]

In doWork () ottieni il valore List dalla mappa in modo thread-safe. In seguito, tuttavia, accedi a tale elenco in una questione non sicura. Ad esempio, un thread potrebbe utilizzare l'elenco in doWork () mentre un altro thread invoca synchronizedMap.get (key) .add (value) in addToMap () . Questi due accessi non sono sincronizzati. La regola pratica è che le garanzie thread-safe di una raccolta non si estendono alle chiavi o ai valori archiviati.

Puoi risolvere questo problema inserendo un elenco sincronizzato nella mappa come

List<String> valuesList = new ArrayList<String>();
valuesList.add(value);
synchronizedMap.put(key, Collections.synchronizedList(valuesList)); // sync'd list

In alternativa puoi sincronizzare sulla mappa mentre accedi all'elenco in doWork () :

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      synchronized (synchronizedMap) {
          //do something with values
      }
    }
  }

L'ultima opzione limiterà un po 'la concorrenza, ma è un po' più chiara IMO.

Inoltre, una breve nota su ConcurrentHashMap. Questa è una classe davvero utile, ma non è sempre un sostituto appropriato per le HashMap sincronizzate. Citando dai suoi Javadoc,

Questa classe è completamente interoperabile con Hashtable nei programmi che si basano sulla sua thread safety ma non sui dettagli di sincronizzazione .

In altre parole, putIfAbsent () è ottimo per gli inserimenti atomici ma non garantisce che altre parti della mappa non cambieranno durante quella chiamata; garantisce solo atomicità. Nel tuo programma di esempio, ti affidi ai dettagli di sincronizzazione di una HashMap (sincronizzata) per cose diverse da put () s.

Ultima cosa. :) Questa fantastica citazione di Java Concurrency in Practice mi aiuta sempre nella progettazione di programmi multi-thread per il debug.

Per ogni variabile di stato mutabile a cui può accedere più di un thread, tutti gli accessi a quella variabile devono essere eseguiti con lo stesso blocco.


Capisco il tuo punto sul bug se stavo accedendo all'elenco con synchronizedMap.get (). Dato che sto usando remove (), la prossima aggiunta con quella chiave non dovrebbe creare un nuovo ArrayList e non interferire con quello che sto usando in doWork?
Ryan Ahearn

Corretta! Ho superato la tua rimozione.
JLR

1
Per ogni variabile di stato mutabile a cui può accedere più di un thread, tutti gli accessi a quella variabile devono essere eseguiti con lo stesso blocco. ---- Generalmente aggiungo una proprietà privata che è solo un nuovo Object () e la uso per i miei blocchi di sincronizzazione. In questo modo so che è tutto grezzo per quel contesto. sincronizzato (objectInVar) {}
AnthonyJClink

11

Sì, stai sincronizzando correttamente. Lo spiegherò in modo più dettagliato. È necessario sincronizzare due o più chiamate di metodo sull'oggetto synchronizedMap solo nel caso in cui si debba fare affidamento sui risultati di precedenti chiamate di metodo nella successiva chiamata di metodo nella sequenza di chiamate di metodo sull'oggetto synchronizedMap. Diamo un'occhiata a questo codice:

synchronized (synchronizedMap) {
    if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
    }
    else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
    }
}

In questo codice

synchronizedMap.get(key).add(value);

e

synchronizedMap.put(key, valuesList);

le chiamate al metodo si basano sul risultato del precedente

synchronizedMap.containsKey(key)

chiamata al metodo.

Se la sequenza delle chiamate al metodo non fosse sincronizzata, il risultato potrebbe essere sbagliato. Ad esempio thread 1sta eseguendo il metodo addToMap()e thread 2sta eseguendo il metodo doWork() La sequenza di chiamate al metodo synchronizedMapsull'oggetto potrebbe essere la seguente: Thread 1ha eseguito il metodo

synchronizedMap.containsKey(key)

e il risultato è " true". Dopo che il sistema operativo ha commutato il controllo dell'esecuzione in thread 2ed è stato eseguito

synchronizedMap.remove(key)

Dopo che il controllo dell'esecuzione è stato ripristinato su thread 1ed è stato eseguito, ad esempio

synchronizedMap.get(key).add(value);

credendo che l' synchronizedMapoggetto contenga il keye NullPointerExceptionverrà lanciato perché synchronizedMap.get(key) tornerà null. Se la sequenza delle chiamate al metodo synchronizedMapsull'oggetto non dipende dai risultati l'una dell'altra, non è necessario sincronizzare la sequenza. Ad esempio non è necessario sincronizzare questa sequenza:

synchronizedMap.put(key1, valuesList1);
synchronizedMap.put(key2, valuesList2);

Qui

synchronizedMap.put(key2, valuesList2);

la chiamata al metodo non si basa sui risultati del precedente

synchronizedMap.put(key1, valuesList1);

chiamata al metodo (non importa se qualche thread ha interferito tra le due chiamate al metodo e per esempio ha rimosso il key1).


4

Mi sembra corretto. Se dovessi cambiare qualcosa, smetterei di usare Collections.synchronizedMap () e sincronizzerei tutto allo stesso modo, solo per renderlo più chiaro.

Inoltre, lo sostituirei

  if (synchronizedMap.containsKey(key)) {
    synchronizedMap.get(key).add(value);
  }
  else {
    List<String> valuesList = new ArrayList<String>();
    valuesList.add(value);
    synchronizedMap.put(key, valuesList);
  }

con

List<String> valuesList = synchronziedMap.get(key);
if (valuesList == null)
{
  valuesList = new ArrayList<String>();
  synchronziedMap.put(key, valuesList);
}
valuesList.add(value);

3
La cosa da fare. Non capisco perché dovremmo usare le Collections.synchronizedXXX()API quando dobbiamo ancora sincronizzare qualche oggetto (che sarà solo la raccolta stessa nella maggior parte dei casi) nella logica della nostra app quotidiana
kellogs,

3

Il modo in cui ti sei sincronizzato è corretto. Ma c'è un problema

  1. Il wrapper sincronizzato fornito dal framework Collection garantisce che le chiamate al metodo Ie add / get / contains verranno eseguite in modo mutuamente esclusivo.

Tuttavia, nel mondo reale, in genere interrogheresti la mappa prima di inserire il valore. Quindi dovresti fare due operazioni e quindi è necessario un blocco sincronizzato. Quindi il modo in cui l'hai usato è corretto. Però.

  1. Avresti potuto utilizzare un'implementazione simultanea di Map disponibile nel framework Collection. Il vantaggio di "ConcurrentHashMap" è

un. Ha un'API 'putIfAbsent' che farebbe le stesse cose ma in modo più efficiente.

b. È efficiente: dThe CocurrentMap blocca solo le chiavi, quindi non blocca il mondo dell'intera mappa. Dove come hai bloccato chiavi e valori.

c. Potresti aver passato il riferimento del tuo oggetto mappa da qualche altra parte nella tua base di codice dove tu / altri sviluppatori nel tuo tean potresti finire per usarlo in modo errato. Vale a dire che può semplicemente aggiungere () o ottenere () senza bloccare l'oggetto della mappa. Quindi la sua chiamata non verrà eseguita mutuamente esclusiva per il blocco di sincronizzazione. Ma l'utilizzo di un'implementazione simultanea ti dà la certezza che non può mai essere utilizzata / implementata in modo errato.


2

Visualizza il Google Collezioni ' Multimap, ad esempio, a pagina 28 di questa presentazione .

Se non puoi usare quella libreria per qualche motivo, considera l'utilizzo ConcurrentHashMapinvece di SynchronizedHashMap; ha un putIfAbsent(K,V)metodo ingegnoso con il quale puoi aggiungere atomicamente l'elenco degli elementi se non è già lì. Inoltre, considera l'utilizzo CopyOnWriteArrayListper i valori della mappa se i tuoi schemi di utilizzo lo richiedono.

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.