Esercizio - Singleton, thread e riflessione (esempi del corso)
Esercizio basato sugli esempi del corso (cartella singleton: Singleton, Main) e sulle slide e dispense sul Singleton (il bollitore di cioccolato). Teoria: Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e PrototypeI pattern creazionali isolano la creazione degli oggetti dal codice che li usa: Singleton garantisce un'unica istanza (costruttore privato, metodo statico getInstance; la versione lazy ingenua non è thread-safe, si correggono con synchronized, inizializzazione eager, double-checked locking con volatile, holder idiom o enum); Factory Method lascia alle sottoclassi la scelta della classe da istanziare; Abstract Factory crea famiglie coerenti di prodotti correlati tramite un'interfaccia di fabbrica; Builder costruisce passo passo oggetti complessi con campi facoltativi; Prototype crea oggetti clonando un prototipo (copia profonda delle parti mutabili); la Simple Factory non è un pattern GoF ma sposta il codice di creazione in una classe sola.Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype →, Java - generics, reflection e passaggio dei parametriIn Java ogni dato ha una propria identità a tempo di esecuzione (il cast è controllato e può lanciare ClassCastException, a differenza del C) e i parametri si passano sempre per valore (di un riferimento, per gli oggetti: si può modificare l'oggetto ma non riassegnare il riferimento del chiamante); i generics (CodaG
Testo
Il Singleton del corso è un contatore con getInstance(), add() (restituisce il valore e incrementa) e getCount():
public class Singleton {
private int myCount = 0;
private static Singleton myInstance = null;
private Singleton() { System.out.println("Sono nato!"); }
public static Singleton getInstance() {
if (myInstance == null) { myInstance = new Singleton(); }
return myInstance;
}
public int add() { return myCount++; }
...
}Si richiede di: 1) descrivere che cosa stampa il Main del corso (tre getInstance(), due add()); 2) mostrare che la versione non è thread-safe e correggerla in tre modi (metodo synchronized, double-checked locking, holder idiom) spiegando i compromessi; 3) trovare gli errori delle slide sul lazy Singleton e sul double-checked locking; 4) mostrare che la riflessione può creare una seconda istanza e che un enum è immune; 5) dire quando non si deve usare un Singleton.
Soluzione
1. Che cosa stampa Main
Il Main del corso chiama getInstance() (la prima volta: stampa Sono nato!) e stampa l'oggetto con toString(), poi esegue Singleton.getInstance().add() e ristampa, poi di nuovo getInstance(), add() e stampa; tutte le chiamate restituiscono lo stesso oggetto, quindi "Sono nato!" compare una sola volta e le tre righe del toString() mostrano lo stesso hashCode con il conteggio 0, poi 1, poi 2. Nel programma sotto: dopo due add() da riferimenti diversi getCount() vale 2, e s == getInstance() è true.
2. Non è thread-safe
Con due thread che chiamano getInstance() quando myInstance è ancora null, entrambi vedono null ed entrambi eseguono new Singleton(): nascono due istanze (e il costruttore stampa due volte). Il programma SingletonVarianti (Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e PrototypeI pattern creazionali isolano la creazione degli oggetti dal codice che li usa: Singleton garantisce un'unica istanza (costruttore privato, metodo statico getInstance; la versione lazy ingenua non è thread-safe, si correggono con synchronized, inizializzazione eager, double-checked locking con volatile, holder idiom o enum); Factory Method lascia alle sottoclassi la scelta della classe da istanziare; Abstract Factory crea famiglie coerenti di prodotti correlati tramite un'interfaccia di fabbrica; Builder costruisce passo passo oggetti complessi con campi facoltativi; Prototype crea oggetti clonando un prototipo (copia profonda delle parti mutabili); la Simple Factory non è un pattern GoF ma sposta il codice di creazione in una classe sola.Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype →) lo mostra con 8 thread che partono insieme e un costruttore reso lento con sleep: la versione ingenua crea fino a 8 istanze, il double-checked locking esattamente 1.
| correzione | codice | compromesso |
|---|---|---|
metodo synchronized |
public static synchronized Singleton getInstance() |
corretta e semplice; ogni chiamata paga il lock (nel corso: fino a 100 volte più lenta) |
| double-checked locking | primo controllo senza lock; synchronized (Singleton.class); secondo controllo; campo volatile |
lazy e veloce dopo la creazione; volatile è indispensabile, e il codice è delicato |
| holder idiom | classe interna statica Contenitore { static final Singleton ISTANZA = new Singleton(); } e getInstance() che la legge |
lazy (la classe interna si carica alla prima chiamata), thread-safe per le garanzie di caricamento della classe, senza lock e senza volatile |
| (eager) | private static final Singleton ISTANZA = new Singleton(); |
thread-safe, ma creata anche se non serve |
3. Errori delle slide
- Lazy sbagliato. La slide scrive
if (uniqueInstance == null) { return new ChocolateBoiler(); } else { return uniqueInstance; }: l'istanza creata non è mai assegnata al campo, quindiuniqueInstancerestanulle ogni chiamata crea un oggetto nuovo (getInstance() == getInstance()dàfalse: provato). Il Singleton non è affatto unico, a prescindere dai thread. Lo stesso errore è nella versionesynchronizeddella slide. La dispensa riporta la versione corretta conuniqueInstance = new ChocolateBoiler();. - Double-checked locking non compilabile. Nel codice della dispensa
return uniqueInstance;sta dentro l'if (uniqueInstance == null): se l'istanza esiste, il metodo non restituisce nulla e il compilatore dàmissing return statement(provato). Ilreturnva fuori dall'if. toStringdel corso: il testo"... <" + hashCode() + " e valgo " + myCountapre una<senza chiuderla; nel programma sotto è corretto.
4. Riflessione e enum
getDeclaredConstructor() trova il costruttore privato e setAccessible(true) ne permette l'uso: newInstance() crea una seconda istanza (che stampa "Sono nato!" di nuovo, con conteggio 0): il Singleton non è più unico. Un enum con una sola costante è immune: la riflessione rifiuta di creare istanze (IllegalArgumentException: Cannot reflectively create enum objects), e la serializzazione di un enum restituisce sempre la costante esistente.
import java.lang.reflect.Constructor;
/** Il Singleton dell'esempio del corso (lazy, non thread-safe), con la correzione alla toString. */
class SingletonCorso {
private int myCount = 0;
private static SingletonCorso myInstance = null;
private SingletonCorso() { System.out.println("Sono nato!"); }
public static SingletonCorso getInstance() {
if (myInstance == null) {
myInstance = new SingletonCorso();
}
return myInstance;
}
public int add() { return myCount++; }
public int getCount() { return myCount; }
@Override public String toString() { return "contatore singleton <" + hashCode() + "> vale " + myCount; } // il corso: manca il '>'
}
/** Versione robusta: enum (thread-safe, serializzazione sicura, immune alla riflessione). */
enum ContatoreEnum {
ISTANZA;
private int conteggio = 0;
int add() { return conteggio++; }
int get() { return conteggio; }
}
public class Singleton {
public static void main(String[] args) throws Exception {
SingletonCorso s = SingletonCorso.getInstance();
s.add();
SingletonCorso.getInstance().add();
System.out.println("conteggio dopo due add() da riferimenti diversi: " + s.getCount());
System.out.println("stessa istanza? " + (s == SingletonCorso.getInstance()));
// la riflessione apre il costruttore privato e crea un secondo "singleton"
Constructor<SingletonCorso> k = SingletonCorso.class.getDeclaredConstructor();
k.setAccessible(true);
SingletonCorso intruso = k.newInstance();
System.out.println("istanza creata con la riflessione e' la stessa? " + (intruso == s) + ", conteggio intruso = " + intruso.getCount());
ContatoreEnum.ISTANZA.add();
ContatoreEnum.ISTANZA.add();
System.out.println("enum: " + ContatoreEnum.ISTANZA.get());
try {
Constructor<ContatoreEnum> ke = ContatoreEnum.class.getDeclaredConstructor(String.class, int.class);
ke.setAccessible(true);
ke.newInstance("ALTRO", 1);
} catch (IllegalArgumentException e) {
System.out.println("enum protetto: " + e.getMessage());
}
}
}Uscita:
Sono nato!
conteggio dopo due add() da riferimenti diversi: 2
stessa istanza? true
Sono nato!
istanza creata con la riflessione e' la stessa? false, conteggio intruso = 0
enum: 2
enum protetto: Cannot reflectively create enum objects5. Quando non usarlo
Il Singleton è una variabile globale travestita: crea dipendenze nascoste (le classi lo chiamano da dentro, e non lo dichiarano nel costruttore), rende i test difficili (non si sostituisce con un oggetto finto e lo stato passa da un test all'altro), e con più thread richiede cura (stato modificabile condiviso: servono lock o strutture sicure, vedi Concorrenza in Java - thread, sincronizzazione e poolIn Java un thread si crea con Thread e Runnable e si avvia con start() (run() chiamato direttamente non crea nessun thread); i thread che modificano dati condivisi producono race condition (aggiornamenti persi) se la sequenza lettura-modifica-scrittura non è atomica, e si protegge con synchronized (monitor dell'oggetto, rientrante), con classi atomiche o con lock; wait/notifyAll dentro un synchronized, sempre in un ciclo while, realizzano l'attesa di una condizione (pool di risorse, produttore-consumatore); l'ordine opposto di acquisizione di due lock provoca stallo (deadlock) e si evita con un ordine globale; i thread pool (ExecutorService, Callable, Future) riusano pochi thread per molti compiti; i pattern concorrenti ricorrenti sono monitor, produttore-consumatore, thread pool, Singleton thread-safe, oggetti immutabili, future.Concorrenza in Java - thread, sincronizzazione e pool →). Va usato per risorse realmente uniche (configurazione di sola lettura, gestore di un dispositivo); spesso è meglio creare un solo oggetto all'avvio e passarlo agli altri (dependency injection).
Versione ripasso
Maindel corso:getInstance()tre volte e dueadd(): una sola rigaSono nato!, tre righe ditoString()con lo stessohashCodee conteggio 0, 1, 2. Programma:getCount()dopo dueadd()da riferimenti diversi = 2;s == getInstance()true.- Non thread-safe: due thread vedono
nulle creano due istanze (8 thread: fino a 8 istanze; double-checked: 1).
| correzione | compromesso |
|---|---|
metodo synchronized |
corretta; ogni chiamata paga il lock (fino a 100 volte più lenta) |
double-checked locking + volatile |
lazy e veloce; volatile indispensabile |
| holder idiom (classe interna statica) | lazy, senza lock, thread-safe per il caricamento della classe |
eager (static final) |
thread-safe, creata anche se non serve |
private static class Contenitore { static final Singleton ISTANZA = new Singleton(); }
public static Singleton getInstance() { return Contenitore.ISTANZA; } // holder idiom- Errori delle slide: lazy con
return new ChocolateBoiler()senza assegnare: ogni chiamata crea un oggetto (getInstance() == getInstance()èfalse), anche nella versionesynchronized; double-checked locking conreturndentro l'if:missing return statement;toStringsenza>di chiusura. - Riflessione:
getDeclaredConstructor()+setAccessible(true)+newInstance()crea una seconda istanza (Sono nato!ancora, conteggio 0;intruso == sèfalse); l'enumè immune (IllegalArgumentException: Cannot reflectively create enum objects) e sicuro anche con la serializzazione. - Quando non usarlo: è una variabile globale travestita: dipendenze nascoste, test difficili (non sostituibile, stato condiviso tra test), con più thread serve sincronizzare lo stato; usarlo per risorse realmente uniche; meglio un solo oggetto creato all'avvio e passato agli altri (dependency injection).
- Errori tipici: lazy senza sincronizzazione; double-checked senza
volatile; assegnare dimenticando di tenere l'istanza nel campo; credere che il costruttore privato basti contro la riflessione; Singleton con stato modificabile condiviso senza lock.