Il tuo linguaggio è sicuro se e solo se il riferimento a HashMap
è pubblicato in modo sicuro . Piuttosto che qualsiasi cosa relativa agli interni di HashMap
se stessa, la pubblicazione sicura si occupa di come il thread di costruzione rende visibile il riferimento alla mappa ad altri thread.
Fondamentalmente, l'unica razza possibile qui è tra la costruzione del HashMap
e tutti i thread di lettura che possono accedervi prima che sia completamente costruito. Gran parte della discussione riguarda ciò che accade allo stato dell'oggetto mappa, ma questo è irrilevante poiché non lo si modifica mai, quindi l'unica parte interessante è come HashMap
viene pubblicato il riferimento.
Ad esempio, immagina di pubblicare la mappa in questo modo:
class SomeClass {
public static HashMap<Object, Object> MAP;
public synchronized static setMap(HashMap<Object, Object> 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 verificare la presenza di null in questo modo:
HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
.. use the map
} else {
.. some default behavior
}
Questo non è sicuro anche se probabilmente appare come se lo fosse. Il problema è che non c'è è accade-prima relazione tra l'insieme di SomeObject.MAP
e la successiva lettura su un altro filo, in modo che il filo di lettura è libero di visualizzare una mappa parzialmente costruito. Questo può praticamente fare qualsiasi cosa e anche in pratica fa cose come mettere il thread di lettura in un ciclo infinito .
Per pubblicare in modo sicuro la mappa, è necessario stabilire un accade-prima di relazione tra la scrittura del riferimento al HashMap
(vale a dire, la pubblicazione ) e le successive lettori di quella di riferimento (ad esempio, il consumo). Per comodità, ci sono solo un paio di facile da ricordare i modi per realizzare che [1] :
- Scambiare il riferimento attraverso un campo correttamente bloccato ( JLS 17.4.5 )
- Utilizzare l'inizializzatore statico per eseguire gli archivi di inizializzazione ( JLS 12.4 )
- Scambiare il riferimento tramite un campo volatile ( JLS 17.4.5 ), o come conseguenza di questa regola, tramite le classi AtomicX
- 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 trasformi la dichiarazione di MAP
:
public static volatile HashMap<Object, Object> MAP;
allora tutto è kosher: lettori che vedono non nullo valore hanno necessariamente accade-prima relazione con il negozio per MAP
e quindi vedere tutti i punti vendita associati con la mappa di inizializzazione.
Gli altri metodi cambiano la semantica del tuo metodo, poiché sia (2) (usando l'inizializzatore statico) che (4) (usando final ) implicano che non puoi impostare MAP
dinamicamente in fase di esecuzione. Se non è necessario farlo, dichiarare semplicemente MAP
come static final HashMap<>
e si garantisce una pubblicazione sicura.
In pratica, le regole sono semplici per un accesso sicuro a "oggetti mai modificati":
Se stai pubblicando un oggetto che non è intrinsecamente immutabile (come in tutti i campi dichiarati final
) e:
- È già possibile creare l'oggetto che verrà assegnato al momento della dichiarazione a : basta usare un
final
campo (incluso static final
per i membri statici).
- Si desidera assegnare l'oggetto in un secondo momento, dopo che il riferimento è già visibile: utilizzare un campo volatile b .
Questo è tutto!
In pratica, è molto efficiente. L'uso di un static final
campo, ad esempio, consente alla JVM di assumere il valore invariato per la durata del programma e di ottimizzarlo pesantemente. L'uso di un final
campo membro 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 alle letture di passare le letture), ma al momento della compilazione potrebbero non esserci alcune ottimizzazioni e riordini - ma questo l'effetto è generalmente piccolo. In cambio, in realtà ottieni di più di quello che hai richiesto - non solo puoi pubblicarne uno in sicurezza HashMap
, puoi anche archiviare quanti più HashMap
s non modificati vuoi allo stesso riferimento ed essere sicuro che tutti i lettori vedranno una mappa pubblicata in modo sicuro .
Per ulteriori dettagli cruenti, consultare Shipilev o questa FAQ di Manson e Goetz .
[1] Quotazione diretta da Shipilev .
a Sembra complicato, ma intendo dire che è possibile assegnare il riferimento in fase di costruzione, nel punto di dichiarazione o nel costruttore (campi membri) o inizializzatore statico (campi statici).
b Facoltativamente, è possibile utilizzare un synchronized
metodo per ottenere / impostare o un AtomicReference
o qualcosa del genere, ma stiamo parlando del lavoro minimo che è possibile eseguire.
c Alcune architetture con modelli di memoria molto deboli (Sto guardando voi , Alpha) possono richiedono un certo tipo di barriera di lettura prima di una final
lettura - ma questi sono molto rare oggi.