È sicuro ottenere valori da una java.util.HashMap da più thread (nessuna modifica)?

C’è un caso in cui verrà costruita una mappa e, una volta inizializzata, non verrà più modificata. Sarà tuttavia ansible accedere (solo tramite get (chiave)) da più thread. È sicuro usare java.util.HashMap in questo modo?

(Attualmente, sto usando felicemente una java.util.concurrent.ConcurrentHashMap , e non ho alcuna necessità misurata di migliorare le prestazioni, ma sono semplicemente curioso di HashMap se una semplice HashMap sarebbe sufficiente. Quindi, questa domanda non è “Quale dovrei usare? “Né è una domanda sul rendimento, piuttosto la domanda è” Sarebbe al sicuro? “)

Il tuo linguaggio è sicuro se e solo se il riferimento alla HashMap è pubblicato in modo sicuro . Piuttosto che tutto ciò che riguarda l’interno di HashMap stesso, la pubblicazione sicura riguarda il modo in cui il thread di costruzione rende il riferimento alla mappa visibile ad altri thread.

Fondamentalmente, l’unica gara ansible qui è tra la costruzione di HashMap e qualsiasi thread di lettura che possa accedervi prima che sia completamente costruito. La maggior parte della discussione riguarda ciò che accade allo stato dell’object mappa, ma questo è irrilevante dal momento che non lo modifichi mai, quindi l’unica parte interessante è come viene pubblicato il riferimento HashMap .

Ad esempio, immagina di pubblicare la mappa in questo modo:

 class SomeClass { public static HashMap MAP; public synchronized static setMap(HashMap m) { MAP = m; } } 

… e ad un certo punto setMap() viene chiamato con una mappa, e altri thread stanno usando SomeClass.MAP per accedere alla mappa, e controllare null come questo:

 HashMap map = SomeClass.MAP; if (map != null) { .. use the map } else { .. some default behavior } 

Questo non è sicuro anche se probabilmente sembra come se lo fosse. Il problema è che non esiste una relazione tra l’insieme di SomeObject.MAP e la successiva lettura su un altro thread, quindi il thread di lettura è libero di vedere una mappa parzialmente costruita. Questo può praticamente fare qualsiasi cosa e anche nella pratica fa cose come mettere il thread di lettura in un ciclo infinito .

Per pubblicare la mappa in modo sicuro, è necessario stabilire una relazione prima e occorrente tra la scrittura del riferimento su HashMap (cioè la pubblicazione ) e i successivi lettori di tale riferimento (cioè il consumo). Convenientemente, ci sono solo alcuni modi facili da ricordare per farlo [1] :

  1. Scambiare il riferimento attraverso un campo opportunamente bloccato ( JLS 17.4.5 )
  2. Utilizzare l’inizializzatore statico per eseguire i magazzini di inizializzazione ( JLS 12.4 )
  3. Scambiare il riferimento tramite un campo volatile ( JLS 17.4.5 ) o come conseguenza di questa regola, tramite le classi AtomicX
  4. Inizializza il valore in un campo finale ( JLS 17.5 ).

Quelli più interessanti per il tuo scenario sono (2), (3) e (4). In particolare, (3) si applica direttamente al codice che ho sopra: se si trasforma la dichiarazione di MAP in:

 public static volatile HashMap MAP; 

allora tutto è kosher: i lettori che vedono un valore non nullo hanno necessariamente una relazione prima-accade con il negozio in MAP e quindi vedono tutti i negozi associati all’inizializzazione della mappa.

Gli altri metodi cambiano la semantica del tuo metodo, dal momento che entrambi (2) (usando l’initalizzatore statico) e (4) (usando finale ) implicano che non puoi impostare MAP modo dinamico al runtime. Se non hai bisogno di farlo, dichiari semplicemente MAP come static final HashMap<> e ti è garantita una pubblicazione sicura.

In pratica, le regole sono semplici per l’accesso sicuro a “oggetti non modificati”:

Se stai pubblicando un object che non è intrinsecamente immutabile (come in tutti i campi dichiarati final ) e:

  • È già ansible creare l’object che verrà assegnato al momento della dichiarazione a : basta usare un campo final (inclusa la static final per i membri statici).
  • Si desidera assegnare l’object in un secondo momento, dopo che il riferimento è già visibile: utilizzare un campo volatile b .

Questo è tutto!

In pratica, è molto efficiente. L’utilizzo di un campo static final , ad esempio, consente a JVM di assumere il valore invariato per la durata del programma e ottimizzarlo in modo pesante. L’uso di un campo membro final consente alla maggior parte delle architetture di leggere il campo in modo equivalente a un normale campo letto e non inibisce ulteriori ottimizzazioni c .

Infine, l’uso di volatile ha un certo impatto: nessuna barriera hardware è necessaria su molte architetture (come x86, in particolare quelle che non consentono letture di passare letture), ma alcuni ottimizzazione e il riordino potrebbero non verificarsi in fase di compilazione – ma questo effetto è generalmente piccolo. In cambio, in realtà ottieni più di quello che hai chiesto – non solo puoi pubblicare una HashMap modo sicuro, puoi memorizzare tante HashMap non modificate come vuoi con lo stesso riferimento ed essere sicuro che tutti i lettori vedranno un sicuro mappa pubblicata

Per ulteriori dettagli, vedi Shipilev o queste FAQ di Manson e Goetz .


[1] Citando direttamente da shipilev .


a Sembra complicato, ma ciò che intendo è che è ansible assegnare il riferimento al momento della costruzione, sia nel punto di dichiarazione o nel costruttore (campi membro) o nell’inizializzatore statico (campi statici).

b Opzionalmente, puoi usare un metodo synchronized per ottenere / impostare, o un AtomicReference o qualcosa del genere, ma stiamo parlando del lavoro minimo che puoi fare.

c Alcune architetture con modelli di memoria molto deboli (sto osservando te , Alpha) potrebbero richiedere un tipo di barriera di lettura prima di una lettura final , ma oggi sono molto rari.

Jeremy Manson, il dio per quanto riguarda il modello di memoria Java, ha un blog in tre parti su questo argomento, perché in sostanza si sta ponendo la domanda “è sicuro accedere a una HashMap immutabile”, la risposta è sì. Ma devi rispondere al predicato a quella domanda che è: “La mia HashMap è immutabile”. La risposta potrebbe sorprendervi: Java ha un insieme relativamente complesso di regole per determinare l’immutabilità.

Per maggiori informazioni sull’argomento, leggi i post sul blog di Jeremy:

Parte 1 su Immutability in Java: http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html

Parte 2 su Immutability in Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html

Parte 3 su Immutability in Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-3.html

Le letture sono sicure dal punto di vista della sincronizzazione ma non dal punto di vista della memoria. Questo è qualcosa che è ampiamente frainteso tra gli sviluppatori Java incluso qui su Stackoverflow. (Osservare la valutazione di questa risposta per prova).

Se hai altri thread in esecuzione, potrebbero non vedere una copia aggiornata di HashMap se non c’è memoria scritta dal thread corrente. Le scritture di memoria avvengono tramite l’uso di parole chiave sincronizzate o volatili o tramite l’uso di alcuni costrutti di concorrenza di java.

Vedi l’articolo di Brian Goetz sul nuovo modello di memoria Java per i dettagli.

Dopo un po ‘di più, ho trovato questo nel java doc (enfasi mia):

Si noti che questa implementazione non è sincronizzata. Se più thread accedono contemporaneamente a una mappa hash e almeno uno dei thread modifica la mappa in modo strutturale, deve essere sincronizzato esternamente. (Una modifica strutturale è qualsiasi operazione che aggiunge o elimina uno o più mapping, semplicemente cambiando il valore associato a una chiave che un’istanza contiene già non è una modifica strutturale.)

Ciò sembra implicare che sarà sicuro, assumendo che il contrario dell’affermazione sia vero.

Una nota è che in alcune circostanze, un get () da una HashMap non sincronizzata può causare un loop infinito. Questo può accadere se un put () concorrente provoca un rehash della Mappa.

http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html

C’è una svolta importante però. È sicuro accedere alla mappa, ma in generale non è garantito che tutti i thread vedano esattamente lo stesso stato (e quindi i valori) di HashMap. Questo potrebbe accadere su sistemi multiprocessore in cui le modifiche apportate a HashMap da un thread (ad esempio, quello che lo popola) possono essere contenute nella cache della CPU e non verranno viste dai thread in esecuzione su altre CPU, fino a quando non viene eseguita un’operazione di memoria. eseguito assicurando la coerenza della cache. La specifica del linguaggio Java è esplicita su questo: la soluzione è acquisire un blocco (sincronizzato (…)) che emette un’operazione di blocco della memoria. Quindi, se sei sicuro che dopo aver compilato la HashMap ognuno dei thread acquisisca QUALSIASI blocco, allora è OK da quel punto in poi accedere ad HashMap da qualsiasi thread fino a quando HashMap non verrà nuovamente modificato.

Secondo http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Inizializzazione di sicurezza è ansible rendere la tua HashMap un campo finale e dopo la conclusione del costruttore sarà pubblicato in modo sicuro.

… Sotto il nuovo modello di memoria, c’è qualcosa di simile a una relazione prima-esistente tra la scrittura di un campo finale in un costruttore e il caricamento iniziale di un riferimento condiviso a quell’object in un altro thread. …

Quindi lo scenario che stai descrivendo è che hai bisogno di mettere un mucchio di dati in una mappa, e quando hai finito di popolarlo lo consideri immutabile. Un approccio che è “sicuro” (il che significa che stai imponendo di essere trattato come immutabile) è quello di sostituire il riferimento con Collections.unmodifiableMap (originalMap) quando sei pronto a renderlo immutabile.

Per un esempio di come le mappe possono fallire se usate simultaneamente, e la soluzione suggerita che ho citato, controlla questa voce di bug parade: bug_id = 6423457

Tieni presente che anche nel codice a thread singolo, la sostituzione di una ConcurrentHashMap con una HashMap potrebbe non essere sicura. ConcurrentHashMap vieta il null come chiave o valore. HashMap non le proibisce (non chiedere).

Pertanto, nella situazione improbabile che il codice esistente possa aggiungere un valore null alla raccolta durante l’installazione (presumibilmente in un caso di errore di qualche tipo), la sostituzione della raccolta come descritta cambierà il comportamento funzionale.

Detto questo, purché tu non faccia altro che le letture concorrenti da una HashMap siano sicure.

[Modifica: da “letture contemporanee”, voglio dire che non ci sono anche modifiche simultanee.

Altre risposte spiegano come garantire questo. Un modo è rendere la mappa immutabile, ma non è necessaria. Ad esempio, il modello di memoria JSR133 definisce esplicitamente l’avvio di un thread come un’azione sincronizzata, il che significa che le modifiche apportate nel thread A prima che inizi il thread B sono visibili nella thread B.

Il mio intento non è quello di contraddire quelle risposte più dettagliate sul modello di memoria Java. Questa risposta intende sottolineare che anche a parte i problemi di concorrenza, vi è almeno una differenza API tra ConcurrentHashMap e HashMap, che potrebbe scottare anche un programma a thread singolo che ne ha sostituito uno con l’altro.]

http://www.docjar.com/html/api/java/util/HashMap.java.html

ecco la fonte di HashMap. Come puoi vedere, non c’è assolutamente nessun codice di blocco / mutex.

Ciò significa che mentre è ansible leggere da una HashMap in una situazione con multithreading, utilizzerei sicuramente una ConcurrentHashMap se ci fossero più scritture.

È interessante notare che sia .NET HashTable che Dizionario hanno un codice di sincronizzazione incorporato.

Se l’inizializzazione e ogni put sono sincronizzati, si è salvati.

Il codice seguente è salvato perché il classloader si prenderà cura della sincronizzazione:

 public static final HashMap map = new HashMap<>(); static { map.put("A","A"); } 

Il codice seguente è salvato perché la scrittura di volatile si occuperà della sincronizzazione.

 class Foo { volatile HashMap map; public void init() { final HashMap tmp = new HashMap<>(); tmp.put("A","A"); // writing to volatile has to be after the modification of the map this.map = tmp; } } 

Questo funzionerà anche se la variabile membro è definitiva perché anche la finale è volatile. E se il metodo è un costruttore.