Pattern Singleton in Java
Implementa il pattern singleton in Java con approcci eager, lazy ed enum-based in modo sicuro e thread-safe.
Il pattern singleton limita una classe a una singola istanza e fornisce un punto di accesso globale ad essa. Un facade di logging, un registro di configurazione, una cache in-process — sono questi i tipi di cose che si adattano bene. In Java, il pattern ha alcune forme standard, ciascuna con i propri compromessi tra thread-safety e caricamento lazy. Esiste anche un approccio che evita silenziosamente la maggior parte dei problemi.
Una breve avvertenza prima di iniziare. Il "singleton" è notoriamente facile da usare in eccesso — ogni singleton è, in effetti, un globale, e i globali rendono il codice più difficile da testare e da ragionare. La maggior parte delle moderne applicazioni Java preferisce la dependency injection: il framework crea una singola istanza e la passa ai componenti che ne hanno bisogno, senza che nessuno di essi debba chiamare Foo.getInstance(). Ricorri a un singleton esplicito quando DI non è disponibile o è genuinamente eccessivo.
Cosa ogni singleton richiede
Qualsiasi implementazione singleton condivide tre elementi:
- Un costruttore privato, in modo che nessuno esterno alla classe possa chiamare
new. - Un campo statico privato che contiene la singola istanza.
- Un accessor statico pubblico che la restituisce.
Le variazioni riguardano principalmente quando l'istanza viene creata e come l'accesso rimane thread-safe.
Inizializzazione eager
La forma più semplice crea l'istanza al caricamento della classe:
public final class Eager {
private static final Eager INSTANCE = new Eager();
private Eager() {}
public static Eager getInstance() { return INSTANCE; }
}L'inizializzazione della classe nella JVM è garantita come thread-safe e viene eseguita esattamente una volta, quindi INSTANCE viene impostato in modo sicuro senza lock. Usalo quando:
- La costruzione è economica, o sai che avrai sempre bisogno dell'istanza.
- Non ti dispiace pagare il costo al momento del caricamento della classe.
Inizializzazione lazy con double-checked locking
Se la costruzione è costosa e potresti non aver mai bisogno dell'istanza, puoi differirla. La versione lazy ingenua non è thread-safe; quella corretta usa il double-checked locking con volatile:
public final class Lazy {
private static volatile Lazy instance;
private Lazy() {}
public static Lazy getInstance() {
Lazy local = instance; // local read avoids re-reading the volatile field
if (local == null) {
synchronized (Lazy.class) {
local = instance;
if (local == null) {
local = new Lazy();
instance = local;
}
}
}
return local;
}
}volatile è essenziale — senza di esso, un altro thread potrebbe vedere il campo impostato su un riferimento non-null il cui costruttore non è ancora terminato. Complicato, ma corretto.
L'initialization-on-demand holder
La forma lazy più pulita usa una classe nidificata privata. La JVM carica l'holder solo quando qualcuno chiama getInstance() per la prima volta, quindi il lavoro è differito — e le garanzie di class-init della JVM gestiscono la thread safety:
public final class Holder {
private Holder() {}
private static class H {
private static final Holder INSTANCE = new Holder();
}
public static Holder getInstance() { return H.INSTANCE; }
}Nessun synchronized, nessun volatile, nessun double-checked locking — eppure lazy. Questa è la forma lazy da usare nella maggior parte del codice.
Il singleton enum
Il singleton corretto più breve in Java è un enum con una costante:
public enum Config {
INSTANCE;
public String get(String key) { /* ... */ }
}Config.INSTANCE è il singleton. La raccomandazione di Joshua Bloch (Effective Java, Item 3) è che questa è la migliore implementazione di singleton, perché la JVM garantisce:
- Esattamente un'istanza. Gli enum vengono costruiti esattamente una volta per JVM.
- Costruzione thread-safe. Le stesse garanzie di class-init del pattern holder.
- Reflection-safe. La reflection non può invocare il costruttore di un enum; i singleton ordinari possono essere compromessi tramite
Constructor.setAccessible(true). - Serialization-safe. La deserializzazione di un singleton normale può produrre silenziosamente una seconda istanza a meno che tu non gestisca
readResolve. Gli enum sono immuni.
L'unica cosa che non può fare è estendere un'altra classe — gli enum estendono implicitamente java.lang.Enum. Possono comunque implementare interfacce.
Cose che rompono i singleton ingenui
Attenzione a questi — sono il motivo per cui "usa solo un campo statico" non è sempre sufficiente:
- Più class loader. Un singleton è uno-per-classloader, non uno-per-JVM. Nei container che isolano le app con i propri loader, la stessa classe può avere più istanze "the".
- Reflection.
setAccessible(true)piùConstructor.newInstance()possono costruire una seconda istanza di qualsiasi singleton non-enum. Proteggi il costruttore conif (INSTANCE != null) throw ...se questo è un problema reale. - Serializzazione. Un singleton
Serializablenecessita diprivate Object readResolve() { return INSTANCE; }per evitare di produrre una seconda copia ad ogni deserializzazione. - Testing. I singleton sono notoriamente difficili da sostituire con stub o resettare. Preferisci la dependency injection nel codice che prevedi di testare con unit test.
Un esempio pratico
Prossimi passi
Questo conclude la Parte 6 e l'intero tour della programmazione orientata agli oggetti in Java — dalle classi, ereditarietà e polimorfismo attraverso interfacce, enum, record, gerarchie sealed e i metodi che ogni oggetto eredita. La prossima parte si allontana per guardare come il codice Java è organizzato: namespace, il layout del file system che li rispecchia e il meccanismo import che porta i tipi da altri posti nel tuo. Continua con Pacchetti Java.