Java Singleton Design Pattern Best Practices con Esempi

Introduzione

Il pattern Singleton di Java è uno dei pattern di progettazione dei Gang of Four e appartiene alla categoria dei pattern di progettazione creazionali. Dalla definizione, sembra essere un pattern di progettazione semplice, ma quando si tratta di implementazione, presenta molte preoccupazioni.

In questo articolo, impareremo i principi del pattern di progettazione singleton, esploreremo diversi modi per implementare il pattern di progettazione singleton e alcune delle migliori pratiche per il suo utilizzo.

Principi del Pattern Singleton

  • Il pattern Singleton limita l’istanza di una classe e garantisce che esista solo un’istanza della classe nella Java Virtual Machine.
  • La classe singleton deve fornire un punto di accesso globale per ottenere l’istanza della classe.
  • Il pattern Singleton viene utilizzato per il logging, gli oggetti driver, la memorizzazione nella cache e il pool di thread.
  • Il design pattern Singleton è anche utilizzato in altri design pattern come l’Abstract Factory, il Builder, il Prototype, il Facade, ecc.
  • Il design pattern Singleton è utilizzato anche nelle classi core di Java (ad esempio, java.lang.Runtime, java.awt.Desktop).

Implementazione del Pattern Singleton in Java

Per implementare un pattern singleton, abbiamo approcci diversi, ma tutti hanno i seguenti concetti comuni.

  • Costruttore privato per limitare l’istanziazione della classe da altre classi.
  • Variabile statica privata della stessa classe che è l’unica istanza della classe.
  • Metodo statico pubblico che restituisce l’istanza della classe, questo è il punto di accesso globale per il mondo esterno per ottenere l’istanza della classe singleton.

Nelle sezioni successive, apprenderemo diversi approcci all’implementazione del pattern singleton e le considerazioni di progettazione con l’implementazione.

1. Inizializzazione desiderosa

Nell’inizializzazione desiderosa, l’istanza della classe singleton viene creata al momento del caricamento della classe. Lo svantaggio dell’inizializzazione desiderosa è che il metodo viene creato anche se l’applicazione client potrebbe non utilizzarlo. Ecco l’implementazione della classe singleton con inizializzazione statica:

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // costruttore privato per evitare che le applicazioni client utilizzino il costruttore
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance() {
        return instance;
    }
}

Se la tua classe singleton non sta utilizzando molte risorse, questa è l’approccio da seguire. Ma nella maggior parte degli scenari, le classi singleton vengono create per risorse come il sistema di file, le connessioni al database, ecc. Dovremmo evitare l’istanziazione a meno che il client chiami il metodo getInstance. Inoltre, questo metodo non fornisce opzioni per la gestione delle eccezioni.

2. Inizializzazione del blocco statico

L’implementazione dell’inizializzazione del blocco statico è simile all’inizializzazione anticipata, tranne che l’istanza della classe viene creata nel blocco statico che offre l’opzione per la gestione delle eccezioni.

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // inizializzazione del blocco statico per la gestione delle eccezioni
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

    public static StaticBlockSingleton getInstance() {
        return instance;
    }
}

Sia l’inizializzazione anticipata che l’inizializzazione del blocco statico creano l’istanza anche prima che venga utilizzata e questa non è la migliore pratica da utilizzare.

3. Inizializzazione pigra

Il metodo di inizializzazione pigra per implementare il pattern singleton crea l’istanza nel metodo di accesso globale. Ecco il codice di esempio per creare la classe singleton con questo approccio:

package com.journaldev.singleton;

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

    public static LazyInitializedSingleton getInstance() {
        if (instance == null) {
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

L’implementazione precedente funziona bene nel caso di un ambiente a singolo thread, ma quando si tratta di sistemi multithread, può causare problemi se più thread sono all’interno della condizione if contemporaneamente. Ciò distruggerà il pattern singleton e entrambi i thread otterranno istanze diverse della classe singleton. Nella prossima sezione, vedremo diversi modi per creare una classe singleton thread-safe.

4. Singleton Thread Safe

A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:

package com.journaldev.singleton;

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }

}

L’implementazione precedente funziona bene e garantisce la sicurezza tra i thread, ma riduce le prestazioni a causa del costo associato al metodo sincronizzato, sebbene ne abbiamo bisogno solo per i primi thread che potrebbero creare istanze separate. Per evitare questo sovraccarico extra ogni volta, il principio del blocco controllato doppio viene utilizzato. In questo approccio, il blocco sincronizzato viene utilizzato all’interno della condizione if con un controllo aggiuntivo per assicurare che venga creata solo una istanza della classe singleton. Il seguente frammento di codice fornisce l’implementazione del blocco controllato doppio:

public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
    if (instance == null) {
        synchronized (ThreadSafeSingleton.class) {
            if (instance == null) {
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

Continua il tuo apprendimento con Classe Singleton Thread Safe.

5. Implementazione Singleton di Bill Pugh

Prima di Java 5, il modello di memoria di Java aveva molti problemi, e gli approcci precedenti tendevano a fallire in determinati scenari in cui troppi thread cercavano di ottenere l’istanza della classe singleton contemporaneamente. Quindi Bill Pugh ha ideato un approccio diverso per creare la classe singleton utilizzando una classe di supporto statica interna. Ecco un esempio dell’implementazione del Singleton di Bill Pugh:

package com.journaldev.singleton;

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

Osserva la classe interna statica privata che contiene l’istanza della classe singleton. Quando la classe singleton viene caricata, la classe SingletonHelper non viene caricata in memoria e viene caricata solo quando qualcuno chiama il metodo getInstance(), questa classe viene caricata e crea l’istanza della classe singleton. Questo è l’approccio più ampiamente utilizzato per la classe singleton in quanto non richiede sincronizzazione.

6. Utilizzo della riflessione per distruggere il Pattern Singleton

La riflessione può essere utilizzata per distruggere tutti gli approcci di implementazione singleton precedenti. Ecco un esempio di classe:

package com.journaldev.singleton;

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                // Questo codice distruggerà il pattern singleton
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

Quando esegui la classe di test precedente, noterai che l’hashCode delle due istanze non è lo stesso, il che distrugge il pattern singleton. La riflessione è molto potente e utilizzata in molti framework come Spring e Hibernate. Continua il tuo apprendimento con il Tutorial di Riflessione in Java.

7. Singleton Enum

Per superare questa situazione con la riflessione, Joshua Bloch suggerisce di utilizzare enum per implementare il pattern di progettazione singleton poiché Java garantisce che qualsiasi valore enum venga istanziato solo una volta in un programma Java. Dal momento che i valori di Enum Java sono globalmente accessibili, anche il singleton lo è. Lo svantaggio è che il tipo enum è in qualche modo inflessibile (ad esempio, non consente l’inizializzazione pigra).

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // fai qualcosa
    }
}

8. Serializzazione e Singleton

A volte, nei sistemi distribuiti, è necessario implementare l’interfaccia Serializable nella classe singleton in modo da poter memorizzare il suo stato nel file system e recuperarlo in un secondo momento. Ecco una piccola classe singleton che implementa anche l’interfaccia Serializable:

package com.journaldev.singleton;

import java.io.Serializable;

public class SerializedSingleton implements Serializable {

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}

    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }

}

Il problema con la classe singleton serializzata è che ogni volta che la deserializziamo, verrà creato una nuova istanza della classe. Ecco un esempio:

package com.journaldev.singleton;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        
// deserializza da file a oggetto

        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());

    }

}

Questo codice produce questo output:

Output
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522

Quindi distrugge il pattern singleton. Per superare questo scenario, tutto ciò che dobbiamo fare è fornire l’implementazione del metodo readResolve().

protected Object readResolve() {
    return getInstance();
}

Dopo ciò, noterai che l’hashcode di entrambe le istanze è lo stesso nel programma di test.

Leggi di più su Java Serialization e Java Deserialization.

Conclusioni

Questo articolo ha trattato il design pattern singleton.

Continua il tuo apprendimento con altri tutorial di Java.

Source:
https://www.digitalocean.com/community/tutorials/java-singleton-design-pattern-best-practices-examples