W3docs

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 con if (INSTANCE != null) throw ... se questo è un problema reale.
  • Serializzazione. Un singleton Serializable necessita di private 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

java— editable, runs on the server

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.

Esercitazione

Pratica
Perché il singleton enum (`enum X { INSTANCE; ... }`) è generalmente considerato la migliore implementazione di singleton in Java?
Perché il singleton enum (`enum X { INSTANCE; ... }`) è generalmente considerato la migliore implementazione di singleton in Java?
Was this page helpful?