Salta al contenuto
Note per Studenti Esercizio - Singleton, thread e riflessione (esempi del corso)

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) spostano i controlli di tipo a tempo di compilazione ma sono cancellati dalla type erasure, quindi non sono i template del C++; la riflessione (Class, Constructor, Method, Field) permette di caricare classi per nome, creare oggetti, chiamare metodi e leggere o scrivere campi a tempo di esecuzione, con eccezioni specifiche (ClassNotFoundException, InvocationTargetException, IllegalAccessException) e il costo di aggirare il controllo statico.Java - generics, reflection e passaggio dei parametri →, 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 →. Il codice è stato compilato ed eseguito con JDK 21.

Testo

Il Singleton del corso è un contatore con getInstance(), add() (restituisce il valore e incrementa) e getCount():

java
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, quindi uniqueInstance resta null e ogni chiamata crea un oggetto nuovo (getInstance() == getInstance() dà false: provato). Il Singleton non è affatto unico, a prescindere dai thread. Lo stesso errore è nella versione synchronized della slide. La dispensa riporta la versione corretta con uniqueInstance = 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). Il return va fuori dall'if.
  • toString del corso: il testo "... <" + hashCode() + " e valgo " + myCount apre 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.

java
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 objects

5. 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

  • Main del corso: getInstance() tre volte e due add(): una sola riga Sono nato!, tre righe di toString() con lo stesso hashCode e conteggio 0, 1, 2. Programma: getCount() dopo due add() da riferimenti diversi = 2; s == getInstance() →\to true.
  • Non thread-safe: due thread vedono null e 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
java
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 versione synchronized; double-checked locking con return dentro l'if: missing return statement; toString senza > 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.

Teoria collegata