Home > GOF Pattern, java > GoF Patterns: Singleton

GoF Patterns: Singleton

20 settembre 2010

Translate in English with Google Translate
Il Singleton è un pattern descritto dal celebre libro “Design Pattern” scritto dalla “Gang of Four”. Viene utilizzato quando si ha la necessità di avere una sola istanza di una classe. Ma non sempre ciò avviene. Perchè ? Scopriamolo insieme.

Introduzione ai Design Patterns.

Prima di andare nel dettaglio dell’articolo, faccio una breve introduzione ai Design Patterns presentati in questo libro.

I Pattern vengono divisi in base a 2 criteri: lo scopo e il raggio di azione.

In base allo scopo, i Pattern, si distinguono in 3 gruppi:

  1. Creazionali: pongono attenzione al modo in cui gli oggetti devono essere creati
  2. Strutturali: si riferiscono al modo in cui le classi e gli oggetti vengono composti
  3. Comportamentali: si concentrano sulle interazioni che avvengono tra classi e oggetti

Mentre invece in base al raggio di azione si distinguono in 2 gruppi:

  1. Classi: si concentrano sulla relazione di ereditarietà delle classi, definita a compile-time quindi statica
  2. Oggetti: si focalizzano sulla relazione tra gli oggetti, definita a run-time quindi dinamica

Ecco come si presenta la loro classificazione:

Il Pattern Singleton

Questo pattern viene utilizzato quando si desidera avere un unico punto di accesso. Per esempio si desidera avere solo un Window Manager oppure una sola Coda di Stampa oppure un unico accesso al database.

Il Singleton può essere utilizzato per gestire una licenza software, per esempio nel caso in cui si vuole limitare il numero di connessioni, tale configurazione consente di disporne solo di una connessione.

Inoltre può essere utilizzata se vogliamo creare un contatore unico invocabile da sorgenti diversi.

Non è tutto oro quello che luccica!

Certamente il Singleton non è ben visto da tutti. Perchè? Per vari motivi:

  • Prima di tutto perchè viola il Principio di Sostituzione di Liskov in quanto il Singleton, non consentendo relazioni di ereditarietà, impedisce la sostituibilità di oggetti legati da un vincolo di parentela.
  • Poi perchè non consente l’isolabilità delle classi, non è possibile avere multiple instanze di una classe per poterle testare e quindi per effettuare gli unit-test.

Ma lasciamo queste considerazioni per approfondire comunque il nostro ragionamento e vedere come duplicare il Singleton.

Come creare un Singleton

Partiamo da una classe di base, questa:

public class MySingleton {
   public MySingleton(){
       System.out.println("Singleton instanziata! hashcode: " + hashCode() );
   }
}

In UML possiamo rappresentarla in questo modo:

Ovviamente non è un Singleton, ma adesso trasformiamo la classe in modo che lo diventi.

Cosa dobbiamo fare?

Dobbiamo fare in modo che la nostra classe permetta la generazione di un unica instanza, pertanto deve intercettare tutte le richieste ed assicurare che solo una instanza sia disponibile.

Ma come fare?

In realtà si possono usare varie tecniche, vediamole.

Possiamo assegnare l’instanza del Singleton utilizzando:

  1. un tipo reference pre-inizializzato
  2. l’inizializzazione pigra
  3. una proprietà statica pubblica
  4. la soluzione di Bill Pugh
  5. un Singleton non condiviso

1.Tipo reference pre-inizializzato

In questo modo inizializziamo il Singleton nel momento in cui la classe viene invocata attraverso la definizione di un oggetto statico.

Seguiamo questi passi:

  1. modifichiamo la visibilià del costruttore da “public” a “private”: non sarà più possibile instanzare la classe se non dal suo interno, quindi solo la classe può instanziare se stessa.
  2. inizializziamo un oggetto statico: pertanto viene creato un oggetto statico che instanzia MySingleton con visibilità private, in modo che non possa essere referenziato dall’esterno
  3. definiamo un metodo statico: per otterne una instanza della classe occorre invocare tale metodo statico che ci ritorni l’instanza della classe

In UML otteniamo questo:

In Java invece il codice è questo:

public class MySingleton {
   private static MySingleton mySingleton = new MySingleton();
   public static MySingleton getInstance(){
   return mySingleton;
}
 private MySingleton(){
   System.out.println("Singleton instanziata! hashcode: " + hashCode() );
 }
}

Adesso proviamo ad instanziare la classe MySingleton usando la classe Start:

public class Start {
   public static void main(String[] args) {
       MySingleton.getInstance();
       MySingleton.getInstance();
   }
}

Abbiamo questo output:

Singleton instanziato! hashcode: 7214081

Eh si! Il costruttore della classe MySingleton viene invocato una volta, pertanto verrà stampata solo una volta la stringa “Singleton instanziato!”

2.Inizializzazione pigra

Un altro modo è quello di usare la “lazy inizialization”cioè “l’inizializzazione pigra”. In questo modo l’oggetto statico viene inizializzato solo nel momento in cui serve, ossia nel momento in cui viene invoco il metodo getInstance().

Vediamo come:

public class MySingleton {
   private static MySingleton mySingleton = null;
   public static MySingleton getInstance(){
       if ( mySingleton == null )
           mySingleton = new MySingleton();
       return mySingleton;
   }
   private MySingleton(){
       System.out.println("Singleton instanziato! hashcode: " + hashCode() );
   }
}

Guardando direttamente il codice non si nota una differenza marcata tra le 2 forme di inizializzazione.

In fondo se è presente solo un metodo statico nel Singleton, l’oggetto MySingleton verrà creato solo quando viene invocata l’unico metodo invocabile getInstance() pertanto l’inizializzazione avviene nello stesso momento in entrambi i casi.

Le differenze sono interne, ossia si osservano se si disassembla il codice utilizzato dalla JVM.

Se usiamo l’utility presente nel JDK javap -c possiamo notare queste differenze.

Questo è il codice disassemblato del Singleton “standard”:

public class MySingleton extends java.lang.Object{
public static MySingleton getInstance();
  Code:
   0:	getstatic	#1; //Field mySingleton:LMySingleton;
   3:	ifnonnull	16
   6:	new	#2; //class MySingleton
   9:	dup
   10:	invokespecial	#3; //Method "":()V
   13:	putstatic	#1; //Field mySingleton:LMySingleton;
   16:	getstatic	#1; //Field mySingleton:LMySingleton;
   19:	areturn

static {};
  Code:
   0:	aconst_null
   1:	putstatic	#1; //Field mySingleton:LMySingleton;
   4:	return

}

Mentre invece questo è il codice disassemblato del Singleton pigro, in cui l’esecuzione è leggermente più snella, ma praticamente impercettibile:

public class MySingleton extends java.lang.Object{
public static MySingleton getInstance();
  Code:
   0:	getstatic	#1; //Field mySingleton:LMySingleton;
   3:	areturn

static {};
  Code:
   0:	new	#12; //class MySingleton
   3:	dup
   4:	invokespecial	#13; //Method "":()V
   7:	putstatic	#1; //Field mySingleton:LMySingleton;
   10:	return
}

3.La soluzione di Bill Pugh

Bill Pugh è professore presso l’Università di Maryland in College Park. Ha contribuito alla definizione del modello della memoria nel linguaggio Java tale da essere incorporato nella specifica 1.5.

La soluzione di Bill Pugh prevede la creazione di una classe annidata statica che si occupa di gestire la Singleton.

La classe annidata statica non ha caratteristiche particolari rispetto ad altre classi, è possibile l’accesso ad essa senza avere una instanza alla classe, tutto qua!

Nel nostro caso la classe annidata è private, quindi la sua caratteristica è annulata, pertanto risulta una classe accessibile SOLO dal Singleton e viene gestita intrinsecamente il problema della sincronizzazione senza bisogno di utilizzare il modificatore synchronized

Vediamo come crearla:

 public class MySingleton {

   private MySingleton() {
   }

   private static class MySingletonHolder {
     public static final MySingleton mySingleton = new MySingleton();
   }

   public static MySingleton getInstance() {
     return MySingletonHolder.mySingleton;
   }

 }

Tanto vale che anche questa volta vediamo il codice java disassemblato, cosi vediamo cosa cambia nella pratica:

public class MySingleton extends java.lang.Object{
public static MySingleton getInstance();
  Code:
   0:	getstatic	#3; //Field MySingleton$MySingletonHolder.mySingleton:LMySingleton;
   3:	areturn

MySingleton(MySingleton$1);
  Code:
   0:	aload_0
   1:	invokespecial	#1; //Method "":()V
   4:	return
}

4.Proprietà public static final

Un modo decisamente più semplice di creare il Singleton è quello di esporre una proprietà pubblica pre-inizializzata senza nessun incapsulamento, ovviamente dobbiamo sempre definire il costruttore come “private”.

Vediamo come definire la proprietà del Singleton:

  1. public: consente di poter accedere alla proprietà della Singleton senza dover passare per un metodo statico che la incapsuli
  2. static: permette di creare solo una copia della proprietà
  3. final: impedisce ai Thread di poter modificare la reference della proprietà
public class MySingleton {
   public static final MySingleton mySingleton = new MySingleton();
   private MySingleton(){
       System.out.println("Singleton instanziato! hashcode: " + hashCode() );
   }
}

In realtà questo approccio non è ben visto, per vari motivi:

  1. viola il concetto di information hiding, le proprietà devono essere accessibili tramite metodi sia in estrazione che in modifica
  2. il modificatore “final” garantisce la non modificabilità della reference alla proprietà ma non del suo contenuto, pertanto se il costruttore del Singleton instanzia altre classi, le reference a queste classi sono comunque modificabili, vediamo come:
public class MySingleton {
    public static final MySingleton mySingleton = new MySingleton();
    private final Hashtable hash = new Hashtable();
    private MySingleton()  {
        hash.put("1", "1");
        System.out.println("Singleton instanziato! hashcode: " + hashCode() );
    }
    public Hashtable getHash() {
        return hash;
    }
}

public class Start {
   public static void main(String[] args) {
        System.out.println("Hash originario: "+MySingleton.mySingleton.getHash().get("1"));
        MySingleton.mySingleton.getHash().put("1", "2");
        System.out.println("Hash modificato: "+MySingleton.mySingleton.getHash().get("1"));
    }
}

In questo caso l’output è il seguente:

Singleton instanziato! hashcode: 7214088
Hash originario: 1
Hash modificato: 2

E’ stato facile modificare il valore dell’Hashtable, pertanto alterare il comportamento del Singleton è immediato. L’errore dipende da una cattiva interpretazione del concetto di final, che impedisce la creazione di una nuova referenze del Singleton ma non impedisce la modifica del suo contenuto.

4.Singleton non condiviso

Altri modi per definire un Singleton, diversamente da quanto proposto dalla GoF, è di definire nel costruttore una logica di controllo, in cui consentire la creazione di una sola instanza.

Ecco un esempio:

public class MySingleton {
   private static boolean flag = false;
   public  MySingleton(){
       if (flag)
           throw new RuntimeException();
       else
           flag = true;
       System.out.println("Singleton instanziato! hashcode: " + hashCode() );
   }
   @Override
   public void finalize(){
       flag=false;
   }
}

In questo caso dobbiamo modificare la classe Start:

public class Start {
   public static void main(String[] args) {
        new MySingleton();
        new MySingleton();
   }
}

L’output è questo:

Singleton instanziato! hashcode: 7214018
Exception in thread "main" java.lang.RuntimeException
       at single.MySingleton.(MySingleton.java:9)
       at single.Start.main(Start.java:23)

In questo caso l’invocazione del secondo oggetto MySingleton genera un’eccezione di Runtime.

La particolarità di questo metodo è che non è possibile condividere uno stesso oggetto Singleton, pertanto l’unico Singleton esistente non è condivisibile ma occorre aspettare che il Garbage Collection elimini la prima referenze alla classe Singleton.

Facciamo un esempio per capire quando è possibile utilizzare il Singleton ad opera di un’altra reference.

Per far cio’ è richiesta la modifica della classe Start in quanto dobbiamo invocare la GC quindi aspettiamo 1 secondo per mettere il nostro Thread in attesa e lasciare che il GC entri in azione:

public class Start {
   public static void main(String[] args) {
        new MySingleton();
        Runtime.getRuntime().gc();
        Thread.sleep(1000);
        new MySingleton();
   }
}

L’output è questo:

Singleton instanziato! hashcode: 7214088
Singleton instanziato! hashcode: 5538765

Ok. Il GC è entrato in azione, ha eseguito il finalize() ed ha annullato la reference all’oggetto, quindi abbiamo potuto instanziare un nuovo Singleton.

Vediamo in un articolo successivo come è possibile crackare un Singleton sfruttando alcuni buchi nella programmazione e/o nella conoscenza

Categorie:GOF Pattern, java
  1. 28 aprile 2014 alle 11:46 AM

    Buongiorno, faccio notare la seguente svista:
    Nella tabella riepilogativa sopra riportata il Flyweight viene elencato tra i Patterns comportamentali, mentre nel dettaglio dello stesso, lo si definisce strutturale(strutturale basato su oggetti).

  2. 28 aprile 2014 alle 11:50 AM

    Buongiorno, faccio solo notare che nella tabella sopra riportata il Flyweight viene considerato di tipo comportamentale, mentre nella spiegazione dettagliata si dice che è di tipo strutturale(strutturale basato su oggetti). Sicuramente si tratta di una svista, ma per correttezza ho voluto segnalarglielo ugualmente.

  1. 15 novembre 2010 alle 10:30 AM
I commenti sono chiusi.
%d blogger cliccano Mi Piace per questo: