12/11/2021
Francesco Gnarra
Design Pattern: il creazionale Singleton
Singleton è il design pattern che ogni programmatore deve conoscere. Appartiene alla categoria dei design pattern creazionali ed ha come scopo garantire che per una determinata classe venga creata una sola singola istanza, fornendo contemporaneamente un punto di accesso globale alla stessa. A dimostrazione della sua importanza va evidenziato che tale pattern è utilizzato anche in altri design pattern come Abstract Factory, Builder, Prototype, Facade, ecc.
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.00
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:
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 in 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.
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() );
}
}
L'output prodotto sarà "I'm the Foo instance", sebbene il valore è recuperato 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 contemporaneamente all’interno della condizione "if" – eventualità che in programmazione multi-thread non possiamo escludere, e che comporterebbe l'ottenimento di istanze diverse della classe per diversi thread, 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():
BasicSingleton attraverso il suo metodo factory getInstance():
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:
public class BasicSingleton {
....
// Costruttore invisibile
private BasicSingleton() {
System.out.println("New BasicSingleton instance returned!");
}
...
}
Consideriamo infine il seguente metodo main():
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:
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!
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 EagerSingleton:
public class EagerSingleton {
// Unica istanza della classe
private static EagerSingleton instance = new EagerSingleton();
// Costruttore invisibile
private EagerSingleton() {
System.out.println("New EagerSingleton instance returned!");
}
public static EagerSingleton 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 fornire alcuna possibilità di poter gestire eventuali eccezioni. Svantaggio che comunque può essere risolto spostando l’istanziazione in un blocco static:
public class EagerSingleton {
// Unica istanza della classe
private static EagerSingleton instance ;
static {
try {
instance = new EagerSingleton();
} 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:
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 onerosa 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 che 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:
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.
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.