Il singleton è il design pattern che un qualunque programmatore (java) non può non conoscere. Appartiene alla categoria dei design pattern creazionali, ed ha come scopo quello di garantire che di una determinata classe venga creata una ed una sola istanza, e di fornire un punto di accesso globale a tale istanza. A dimostrazione della sua importanza si consideri che tale pattern è utilizzato anche in altri design pattern come Abstract Factory, Builder, Prototype, Facade, etc.
Il concetto di bean di tipo singleton lo troviamo anche in molti framework che implementano la Dependency Injection come Spring o Guice, ma concettualmente sono abbastanza differenti. In Spring, ad esempio, quando un bean ha scope singleton significa che può esistere una ed una sola istanza di quel bean all’interno del contesto di Spring (IoC container). Sebbene le due definizioni possano sembrare simili, in realtà non lo sono perché, anche se non espressamente specificato, il design pattern singleton implica che l’istanza della classe sia unica per class loader .
Ricordiamo che il class loader crea le istanze di classe all’interno della heap memory space di java (si veda Java Heap Space vs Stack Memory per un approfondimento), area di memoria in cui è anche contenuto il container IoC di Spring.
Implementazione Classica
Il modo più semplice per implementare una classe singleton richiede due requisiti:
- Rendere privato il costruttore della classe.
- Implementare un metodo statico (detto factory) che istanzia e restituisce un oggetto della classe.
Secondo le specifiche date una possibile implementazione della classe singleton sarà quindi la seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class BasicSingleton { // Unica istanza della classe private static BasicSingleton instance = null; // Costruttore invisibile private BasicSingleton() {} public static BasicSingleton getInstance() { // Crea l'oggetto solo se NON esiste: if (instance == null) { instance = new BasicSingleton(); } return instance; } // Variable of type String private String value; /* Getter and Setter form property 'value'*/ } |
Nella classe BasicSingleton
, quando viene invocato per la prima volta il metodo getInstance()
, un oggetto della classe è creato ed associato alla variabile instance
che è poi restituita dal metodo. Poiché instance
è una variabile statica, se si invoca nuovamente il metodo getInstance()
, anch’esso statico, essendo ora instance
non nulla, il metodo restituisce il suo contenuto, invece di istanziare nuovamente la classe BasicSingleton
. Questa parte è implementata dalla condizione if
.
SI noti che i tale implementazione la creazione dell’oggetto singleton è ritardata fino a quando non se ne abbia effettivamente bisogno (Lazy Initialization) ovvero quando il metodo getInstance() è invocato.
A dimostrazione del corretto funzionamento della classe consideriamo il seguente codice in cui sono utilizzate due variabili foo
e bar
di tipo BasicSingleton
.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Main { public static void main(String[] args) { // instantiating Singleton class with variable x BasicSingleton foo = BasicSingleton.getInstance(); // instantiating Singleton class with variable y BasicSingleton bar = BasicSingleton.getInstance(); foo.setValue( "I'm the Foo instance"); System.out.println( bar.getValue() ); } } |
Come si si aspetta l’output prodotto sarà “I'm the Foo instance
” sebbene il valore è secuperato dall’istanza bar
della classe.
Il problema del Multi-threading
L’implementazione vista sopra è corretta fino a quando operiamo in un ambito single-thread, ma in un ambiente multi-thread possono verificarsi comportamenti non corretti nel caso in cui due thread si trovino all’interno della condizione if
contemporaneamente. Eventualità che in programmazione multi-thread non possiamo escludere, e che comporterebbe che diversi thread potrebbero ottenere istanze diverse della classe, rompendo di fatto il pattern singleton.
A dimostrazione di ciò implementiamo un thread che semplicemente istanzia la classe BasicSingleton
attraverso il suo metodo factory getInstance()
:
1 2 3 4 5 6 7 |
public class SingletonThread extends Thread { BasicSingleton singleton; public void run() { singleton = BasicSingleton.getInstance(); } } |
Modifichiamo poi il costruttore della classeBasicSingleton
inserendo la stampa di un messaggio su console, in modo da evidenziare il fatto che uno oggetto della classe è stato creato:
1 2 3 4 5 6 7 8 9 10 |
public class BasicSingleton { .... // Costruttore invisibile private BasicSingleton() { System.out.println("New BasicSingleton instance returned!"); } ... } |
main()
:
1 2 3 4 5 6 7 |
public static void main(String[] args) { for ( int i = 1; i <= 10; i++ ) { SingletonThread thread = new SingletonThread(); thread.setName( "Thread " + i ); thread.start(); } } |
Quello che ci aspetteremmo di avere è la stampa su console di un solo messaggio. Ma eseguendo il programma diverse volte noteremo che in ogni esecuzione il messaggio è stampato più volte, prova del fatto che alcuni thread riescono a istanziare un oggetto diverso della classe, rompendo di fatto il pattern:
1 2 3 4 5 6 7 |
New BasicSingleton instance returned! New BasicSingleton instance returned! New BasicSingleton instance returned! New BasicSingleton instance returned! New BasicSingleton instance returned! New BasicSingleton instance returned! New BasicSingleton instance returned! |
Per poter rendere l’implementazione thread safe possiamo adottare diverse soluzioni alternative, che sono mostrate nei paragrafi seguenti.
Eager Singleton
La prima consiste nello spostare la creazione dell’oggetto al di fuori del metodo getInstance()
, di fatto facendo in modo che l’istanza sia generata nel momento in cui la classe è caricata dal class loader. Tale tecnica è implementata nella seguente classe EigerSingleton
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class EigerSingleton { // Unica istanza della classe private static EigerSingleton instance = new EigerSingleton(); // Costruttore invisibile private EigerSingleton() { System.out.println("New EigerSingleton instance returned!"); } public static EigerSingleton getInstance() { // Crea l'oggetto solo se NON esiste: return instance; } } |
Tale approccio è ottimale nel caso in cui la classe singleton non utilizza molte risorse. Sfortunatamente nella maggior parte degli scenari, le classi singleton sono utilizzate per gestire risorse onerose come File System, connessioni a Database, ecc. E’ quindi preferibile ritardare l’istanziazione di tali classi fino a quando il client non ne ha davvero bisogno, ovvero quando è invocato il metodo getInstance()
.
Inoltre, questo approccio ha anche lo svantaggio di non fornisce alcuna possibilità di poter gestire eventuali eccezioni. Svantaggio che comunque può essere risolto spostando l’istanziazione in un blocco static:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class EigerSingleton { // Unica istanza della classe private static EigerSingleton instance ; static { try { instance = new EigerSingleton(); } catch (Exception e) { e.printStackTrace(); } } ... } |
Thread Safe Singleton
La seconda opzione per ottenere un singleton thread-safe è quella di rendere il factory method getInstance()
di tipo syncronized
, come mostrato nella classe ThreadSafeSingleton
riportata di seguito:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class ThreadSafeSingleton { // Unica istanza della classe private static ThreadSafeSingleton instance ; // Costruttore invisibile private ThreadSafeSingleton() { System.out.println("New ThreadSafeSingleton instance returned!"); } public static synchronized ThreadSafeSingleton getInstance() { // Crea l'oggetto solo se NON esiste: if (instance == null) { instance = new ThreadSafeSingleton(); } return instance; } } |
Tale implementazione, sebbene funzioni correttamente, è prestazionalmente oneroso a causa del costo computazionale associato all’esecuzione della direttiva synchronized
. Per evitare l’inconveniente è possibile utilizzare il Double Checked Locking, ovvero il blocco sincronizzato viene utilizzato all’interno della condizione if
con un controllo aggiuntivo per garantire che venga creata una sola istanza della classe singleton. Con tale tecnica il metodo getInstance()
diviene:
1 2 3 4 5 6 7 8 9 10 |
public static ThreadSafeSingleton getInstance() { if (instance == null) { synchronized (ThreadSafeSingleton.class) { if (instance == null){ instance = new ThreadSafeSingleton(); } } } return instance; } |
Bill Pugh Singleton
L’implementazione più utilizzata di classe singleton è quella che fu proposta da William Pugh e che prevede l’utilizzo di una classe helper annidata.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class BillPughSingleton { private BillPughSingleton() { System.out.println("New BillPughSingleton instance returned!"); } private static class SingletonHelper { private static final BillPughSingleton INSTANCE = new BillPughSingleton(); } public static BillPughSingleton getInstance() { return SingletonHelper.INSTANCE; } } |
Quando viene caricata la classe BillPughSingleton,
la classe annidata SingletonHelper
non viene caricata in memoria. Solo quando è invocato il metodo getInstance()
, questa classe viene caricata e conseguentemente viene creata anche l’istanza della classe BillPughSingleton
. Tale implementazione quindi si comporta come la classe EagerSingleton
, ma senza soffrire del problema della inizializzazione anticipata, inoltre è più efficiente della classe ThreadSafeSingleton
, in quanto non fa uso dei blocchi synchronized
.
Codie Sorgente
Il codice sorgente del progetto è disponibile qui singleton.