Blocco doppio controllo Java

Mi è capitato di leggere un articolo che parlava di recente del modello di blocco a doppia verifica in Java e delle sue insidie ​​e ora mi chiedo se una variante di quel modello che uso da anni sia soggetta a problemi.

Ho esaminato molti post e articoli sull’argomento e ho compreso i potenziali problemi con l’ottenimento di un riferimento a un object parzialmente costruito e, per quanto posso dire, non penso che la mia implementazione sia soggetta a questi problemi. Ci sono problemi con il seguente schema?

E, se no, perché la gente non la usa? Non l’ho mai visto raccomandato in nessuna discussione che ho visto in merito a questo problema.

public class Test { private static Test instance; private static boolean initialized = false; public static Test getInstance() { if (!initialized) { synchronized (Test.class) { if (!initialized) { instance = new Test(); initialized = true; } } } return instance; } } 

Il doppio controllo del blocco è rotto . Poiché inizializzato è un primitivo, potrebbe non richiedere che sia volatile per funzionare, tuttavia nulla impedisce che l’inizializzazione venga vista come vera per il codice non sincronizzato prima che l’istanza venga inizializzata.

EDIT: Per chiarire la risposta di cui sopra, la domanda originale chiedeva di usare un booleano per controllare il blocco del doppio controllo. Senza le soluzioni nel link sopra, non funzionerà. Puoi ricontrollare il blocco in realtà impostando un valore booleano, ma hai ancora problemi con il riordino delle istruzioni quando si tratta di creare un’istanza di class. La soluzione suggerita non funziona perché l’istanza potrebbe non essere inizializzata dopo aver visto il booleano inizializzato come vero nel blocco non sincronizzato.

La soluzione corretta per il doppio controllo del blocco è quella di utilizzare volatile (sul campo dell’istanza) e dimenticare il booleano inizializzato e assicurarsi di utilizzare JDK 1.5 o successivo, o inizializzarlo in un campo finale, come elaborato nel collegamento articolo e la risposta di Tom, o semplicemente non usarlo.

Certamente l’intero concetto sembra un enorme e prematuro ottimizzazione a meno che non si sappia che si otterrà una tonnellata di contesa sui thread per ottenere questo Singleton, o se si è profilata l’applicazione e si è visto che questo è un punto caldo.

Ciò funzionerebbe se initialized fosse volatile . Proprio come con synchronized gli effetti interessanti di volatile non sono così tanto da fare con il riferimento come ciò che possiamo dire su altri dati. L’impostazione del campo di instance e dell’object Test è obbligata a verificarsi prima della scrittura su initialized . Quando si utilizza il valore memorizzato nella cache attraverso il cortocircuito, avviene la lettura di initialize , prima della lettura instance e degli oggetti raggiunti tramite il riferimento. Non c’è alcuna differenza significativa nell’avere un flag initialized separato (diverso da quello che causa ancora più complessità nel codice).

(Le regole per i campi final nei costruttori per la pubblicazione non sicura sono leggermente diverse).

Tuttavia, in questo caso dovresti vedere raramente il bug. Le probabilità di mettersi nei guai quando si usano per la prima volta sono minime, ed è una gara non ripetuta.

Il codice è troppo complicato. Potresti semplicemente scriverlo come:

 private static final Test instance = new Test(); public static Test getInstance() { return instance; } 

Il doppio blocco controllato è effettivamente rotto, e la soluzione al problema è in realtà più semplice da implementare in termini di codice rispetto a questo idioma – basta usare un inizializzatore statico.

 public class Test { private static final Test instance = createInstance(); private static Test createInstance() { // construction logic goes here... return new Test(); } public static Test getInstance() { return instance; } } 

È garantito che un inizializzatore statico venga eseguito la prima volta che JVM carica la class e prima che il riferimento alla class possa essere restituito a qualsiasi thread, rendendolo intrinsecamente sicuro.

Questo è il motivo per cui il doppio controllo bloccato è rotto.

Synchronize garantisce che solo un thread può inserire un blocco di codice. Ma non garantisce che le modifiche delle variabili fatte all’interno della sezione sincronizzata saranno visibili ad altri thread. Solo i thread che entrano nel blocco sincronizzato sono garantiti per vedere le modifiche. Questo è il motivo per cui il doppio controllo bloccato è rotto – non è sincronizzato dal lato del lettore. Il thread di lettura potrebbe vedere che il singleton non è nullo, ma i dati di singleton potrebbero non essere completamente inizializzati (visibili).

L’ordine è fornito da volatile . volatile garantisce l’ordine, ad esempio scrivere in campo statico volatile singleton garantisce che le scritture sull’object singleton saranno terminate prima del campo statico di scrittura su volatile. Non impedisce la creazione di singleton di due oggetti, questo è fornito dalla sincronizzazione.

I campi statici finali di class non devono essere volatili. In Java, JVM si prende cura di questo problema.

Vedi il mio post, una risposta al pattern Singleton e il blocco double check rotto in un’applicazione Java reale , che illustra un esempio di un singleton per quanto riguarda il blocco a doppio controllo che sembra intelligente ma rotto.

Probabilmente dovresti usare i tipi di dati atomici in java.util.concurrent.atomic .

Se “inizializzato” è vero, allora “istanza” DEVE essere completamente inizializzata, come 1 più 1 equivale a 2 :). Pertanto, il codice è corretto. L’istanza viene solo istanziata una volta ma la funzione può essere chiamata un milione di volte, quindi migliora le prestazioni senza controllare la sincronizzazione per un milione meno una volta.

Ci sono ancora alcuni casi in cui è ansible utilizzare un doppio controllo.

  1. Innanzitutto, se davvero non hai bisogno di un singolo, e il doppio controllo viene usato solo per NON creare e inizializzare su molti oggetti.
  2. C’è un campo final impostato alla fine del costruttore / blocco inizializzato (che fa sì che tutti i campi precedentemente inizializzati siano visti da altri thread).

Ho studiato l’idioma di blocco a doppio controllo e da quello che ho capito, il tuo codice potrebbe portare al problema di leggere un’istanza parzialmente costruita A MENO CHE la tua class di Test sia immutabile:

Il modello di memoria Java offre una garanzia speciale di sicurezza di inizializzazione per la condivisione di oggetti immutabili.

Possono essere tranquillamente accessibili anche quando la sincronizzazione non viene utilizzata per pubblicare il riferimento all’object.

(Citazioni dal libro molto consigliabile Java Concurrency in Practice)

Quindi in quel caso, il doppio idioma di blocco controllato funzionerebbe.

Ma, se questo non è il caso, osserva che stai restituendo l’istanza della variabile senza sincronizzazione, quindi la variabile di istanza potrebbe non essere completamente costruita (vedresti i valori predefiniti degli attributi invece dei valori forniti nel costruttore).

La variabile booleana non aggiunge nulla per evitare il problema, perché potrebbe essere impostata su true prima che la class Test venga inizializzata (la parola chiave sincronizzata non evita il riordinamento completo, alcune sencenze potrebbero cambiare l’ordine). Non c’è nessuna regola prima del modello di memoria Java per garantirlo.

E rendere il booleano volatile non aggiungerebbe nulla, perché le variabili a 32 bit sono create atomicamente in Java. Il doppio idioma di blocco controllato funzionerebbe anche con loro.

Da Java 5, è ansible risolvere il problema dichiarando la variabile di istanza come volatile.

Puoi leggere di più sull’idioma double check in questo articolo molto interessante .

Infine, alcuni consigli che ho letto:

  • Considerare se è necessario utilizzare il modello singleton. È considerato un anti-modello da molte persone. L’iniezione di dipendenza è preferibile ove ansible. Controlla questo .

  • Considerare attentamente se l’ottimizzazione del blocco con doppia verifica è davvero necessaria prima di implementarla, perché nella maggior parte dei casi non sarebbe valsa la pena di essere valutata. Inoltre, considera la costruzione della class Test nel campo statico, poiché il caricamento lazy è utile solo quando la costruzione di una class richiede molte risorse e, nella maggior parte dei casi, non è il caso.

Se è ancora necessario eseguire questa ottimizzazione, controllare questo collegamento che fornisce alcune alternative per ottenere un effetto simile a quello che si sta tentando.

Il problema DCL è rotto, anche se sembra funzionare su molte macchine virtuali. C’è una bella descrizione del problema qui http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html .

il multithreading e la coerenza della memoria sono soggetti più complicati di quanto potrebbero apparire. […] Puoi ignorare tutta questa complessità se usi semplicemente lo strumento che Java fornisce esattamente per questo scopo: la sincronizzazione. Se si sincronizza ogni accesso a una variabile che potrebbe essere stata scritta o che potrebbe essere letta da un altro thread, non si avranno problemi di coerenza della memoria.

L’unico modo per risolvere questo problema in modo corretto è evitare l’inizializzazione pigra (fatelo ardentemente) o il controllo singolo all’interno di un blocco sincronizzato. L’uso del valore booleano initialized equivale a un controllo Null sul riferimento stesso. Un secondo thread può vedere initialized essere vero ma l’ instance potrebbe essere ancora nulla o parzialmente inizializzata.

Il blocco doppio è l’anti-pattern.

Lazy Initialization Holder Class è lo schema che dovresti guardare.

Nonostante tante altre risposte, ho pensato che avrei dovuto rispondere perché non esiste ancora una risposta semplice che dica perché DCL è rotto in molti contesti, perché non è necessario e cosa invece dovresti fare. Quindi userò una citazione di Goetz: Java Concurrency In Practice che per me fornisce la spiegazione più succinta nel suo capitolo finale sul modello di memoria Java.

Riguarda la pubblicazione sicura delle variabili:

Il vero problema con DCL è il presupposto che la cosa peggiore che può capitare quando si legge un riferimento ad un object condiviso senza sincronizzazione sia di vedere erroneamente un valore scaduto (in questo caso, null); in tal caso l’idioma DCL compensa questo rischio riprovando con il blocco trattenuto. Ma il caso peggiore è in realtà considerevolmente peggiore: è ansible vedere un valore corrente del riferimento ma valori obsoleti per lo stato dell’object, il che significa che l’object potrebbe essere visto in uno stato non valido o errato.

Le modifiche successive apportate al JMM (Java 5.0 e successive) hanno consentito a DCL di funzionare se la risorsa è resa volatile e l’impatto sulle prestazioni di questo è limitato poiché le letture volatili sono di solito solo leggermente più costose delle letture non volatili.

Tuttavia, questo è un idioma la cui utilità è ampiamente passata, le forze che lo hanno motivato (sincronizzazione lenta e non controllata, avvio lento della JVM) non sono più in gioco, rendendolo meno efficace come ottimizzazione. L’idioma di inizializzazione pigro offre gli stessi vantaggi ed è più facile da capire.

Listato 16.6. Idioma di class Titolare di inizializzazione pigro.

 public class ResourceFactory private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource; } } 

Questo è il modo di farlo.

Innanzitutto, per i singleton puoi usare un Enum, come spiegato in questa domanda Implementare Singleton con un Enum (in Java)

In secondo luogo, dal momento che Java 1.5, è ansible utilizzare una variabile volatile con doppio controllo bloccato, come spiegato alla fine di questo articolo: https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html