Perché è volatile utilizzato in questo esempio di blocco a doppio controllo

Dal primo libro di modelli di design, il modello singleton con doppio controllo è stato implementato come di seguito:

public class Singleton { private volatile static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } 

Non capisco perché sia ​​usato volatile . L’utilizzo non volatile non ha lo scopo di utilizzare il doppio controllo, ovvero le prestazioni?

Una buona risorsa per capire perché il volatile è necessario proviene dal libro JCIP . Wikipedia ha una spiegazione decente di questo materiale.

Il vero problema è che il Thread A può assegnare uno spazio di memoria, per instance prima che sia terminata la costruzione instance . Thread B vedrà quell’assegnazione e cercherà di usarla. Ciò si traduce in errore del Thread B poiché sta utilizzando una versione di instance parzialmente costruita.

Come citato da @irreputable, volatile non è costoso. Anche se è costoso, dovrebbe essere data priorità alla coerenza rispetto alle prestazioni.

C’è un altro modo pulito ed elegante per Lazy Singletons.

 public final class Singleton { private Singleton() {} public static Singleton getInstance() { return LazyHolder.INSTANCE; } private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } } 

Fonte articolo: Initialization-on-demand_holder_idiom da wikipedia

Nell’ingegneria del software, l’idioma di Initialization on Demand Holder (design pattern) è un singleton pigro-caricato. In tutte le versioni di Java, l’idioma abilita un’inizializzazione pigro sicura e altamente concorrente con buone prestazioni

Poiché la class non ha alcuna variabile static da inizializzare, l’inizializzazione viene completata in modo triviale.

La definizione di class statica LazyHolder al suo interno non viene inizializzata finché la JVM non determina che LazyHolder deve essere eseguito.

La class statica LazyHolder viene eseguita solo quando il metodo statico getInstance viene richiamato sulla class Singleton e la prima volta che ciò accade la JVM caricherà e inizializzerà la class LazyHolder .

Questa soluzione è thread-safe senza richiedere costrutti di linguaggio speciali (cioè volatile o synchronized ).

Bene, non c’è un blocco a doppia verifica per le prestazioni. È un modello rotto .

Lasciando da parte le emozioni, volatile è qui perché senza di esso quando il secondo thread passa instance == null , il primo thread potrebbe non build ancora new Singleton() ancora: nessuno promette che la creazione dell’object avvenga – prima dell’assegnazione instance per qualsiasi thread ma quello che crea effettivamente l’object.

volatile a sua volta stabilisce la relazione tra prima e prima delle letture e delle scritture, e risolve il modello rotto.

Se stai cercando prestazioni, usa invece la class statica interna del supporto.

Se non ce l’hai, un secondo thread potrebbe entrare nel blocco sincronizzato dopo il primo impostarlo su null, e la tua cache locale penserebbe ancora che fosse null.

Il primo non è per la correttezza (se fosse vero che sarebbe controproducente) ma piuttosto per l’ottimizzazione.

Una lettura volatile non è molto costosa in sé.

È ansible progettare un test per chiamare getInstance() in un ciclo stretto, per osservare l’impatto di una lettura volatile; tuttavia quel test non è realistico; in tale situazione, il programmatore di solito chiamerebbe getInstance() una volta e memorizza nella cache l’istanza per la durata dell’uso.

Un altro impl è usando un campo final (vedi wikipedia). Ciò richiede una lettura aggiuntiva, che può diventare più costosa rispetto alla versione volatile . La versione final potrebbe essere più veloce in un ciclo stretto, tuttavia tale test è discutibile come precedentemente sostenuto.

Dichiarare la variabile come volatile garantisce che tutti gli accessi ad esso effettivamente leggano il suo valore corrente dalla memoria.

Senza volatile , il compilatore può ottimizzare gli accessi alla memoria e mantenere il suo valore in un registro, quindi solo il primo utilizzo della variabile legge la posizione di memoria effettiva che contiene la variabile. Questo è un problema se la variabile viene modificata da un altro thread tra il primo e il secondo accesso; il primo thread ha solo una copia del primo valore (pre-modificato), quindi la seconda istruzione if verifica una copia obsoleta del valore della variabile.