Come navigare nel pattern Singleton apparentemente semplice

Il pattern Singleton è apparentemente semplice, anche e soprattutto per gli sviluppatori Java. In questo classico articolo di JavaWorld , David Geary dimostra come gli sviluppatori Java implementano i singleton, con esempi di codice per multithreading, classloader e serializzazione utilizzando il pattern Singleton. Conclude con uno sguardo all'implementazione dei registri singleton per specificare singleton in fase di esecuzione.

A volte è appropriato avere esattamente un'istanza di una classe: gestori di finestre, spooler di stampa e filesystem sono esempi prototipici. In genere, a questi tipi di oggetti, noti come singleton, si accede da diversi oggetti in un sistema software e pertanto richiedono un punto di accesso globale. Naturalmente, proprio quando sei certo che non avrai mai bisogno di più di un'istanza, è una buona scommessa che cambierai idea.

Il modello di progettazione Singleton affronta tutte queste preoccupazioni. Con il design pattern Singleton puoi:

  • Assicurati che venga creata solo un'istanza di una classe
  • Fornire un punto di accesso globale all'oggetto
  • Consenti più istanze in futuro senza influire sui client di una classe singleton

Sebbene il modello di progettazione Singleton, come evidenziato di seguito dalla figura seguente, sia uno dei modelli di progettazione più semplici, presenta una serie di insidie ​​per lo sviluppatore Java incauto. Questo articolo discute il modello di progettazione Singleton e affronta queste insidie.

Ulteriori informazioni sui modelli di progettazione Java

È possibile leggere tutte le colonne di Java Design Pattern di David Geary o visualizzare un elenco degli articoli più recenti di JavaWorld sui modelli di design Java. Vedere " Design pattern, il quadro generale " per una discussione sui pro e contro dell'uso dei pattern Gang of Four. Voglio di più? Ricevi la newsletter di Enterprise Java consegnata nella tua casella di posta.

Il modello Singleton

In Design Patterns: Elements of Reusable Object-Oriented Software , la Gang of Four descrive il pattern Singleton in questo modo:

Assicurati che una classe abbia una sola istanza e fornisci un punto di accesso globale ad essa.

La figura seguente illustra il diagramma delle classi del modello di progettazione Singleton.

Come puoi vedere, non c'è molto nel design pattern di Singleton. I singleton mantengono un riferimento statico all'unica istanza singleton e restituiscono un riferimento a tale istanza da un instance()metodo statico .

L'esempio 1 mostra un'implementazione del classico design pattern Singleton:

Esempio 1. Il classico singleton

public class ClassicSingleton { private static ClassicSingleton instance = null; protected ClassicSingleton() { // Exists only to defeat instantiation. } public static ClassicSingleton getInstance() { if(instance == null) { instance = new ClassicSingleton(); } return instance; } }

Il singleton implementato nell'esempio 1 è facile da capire. La ClassicSingletonclasse mantiene un riferimento statico all'istanza singleton solitaria e restituisce quel riferimento dal getInstance()metodo statico .

Ci sono diversi punti interessanti riguardanti la ClassicSingletonclasse. In primo luogo, ClassicSingletonutilizza una tecnica nota come istanziazione pigra per creare il singleton; di conseguenza, l'istanza singleton non viene creata fino a quando il getInstance()metodo non viene chiamato per la prima volta. Questa tecnica garantisce che le istanze singleton vengano create solo quando necessario.

In secondo luogo, notare che ClassicSingletonimplementa un costruttore protetto in modo che i client non possano istanziare ClassicSingletonistanze; tuttavia, potresti essere sorpreso di scoprire che il seguente codice è perfettamente legale:

public class SingletonInstantiator { public SingletonInstantiator() { ClassicSingleton instance = ClassicSingleton.getInstance(); ClassicSingleton anotherInstance =new ClassicSingleton(); ... } }

Come può la classe nel frammento di codice precedente, che non si estende, ClassicSingletoncreare ClassicSingletonun'istanza se il ClassicSingletoncostruttore è protetto? La risposta è che i costruttori protetti possono essere chiamati da sottoclassi e da altre classi nello stesso pacchetto . Poiché ClassicSingletone si SingletonInstantiatortrovano nello stesso pacchetto (il pacchetto predefinito), i SingletonInstantiator()metodi possono creare ClassicSingletonistanze. Questo dilemma ha due soluzioni: puoi rendere ClassicSingletonprivato il costruttore in modo che solo i ClassicSingleton()metodi lo chiamino; tuttavia, ciò significa che ClassicSingletonnon può essere sottoclasse. A volte, questa è una soluzione desiderabile; in tal caso, è una buona idea dichiarare la propria classe singletonfinal, che rende esplicita tale intenzione e consente al compilatore di applicare ottimizzazioni delle prestazioni. L'altra soluzione è mettere la tua classe singleton in un pacchetto esplicito, in modo che le classi in altri pacchetti (incluso il pacchetto predefinito) non possano istanziare istanze singleton.

Un terzo punto interessante su ClassicSingleton: è possibile avere più istanze singleton se classi caricate da diversi classloader accedono a un singleton. Questo scenario non è così inverosimile; ad esempio, alcuni contenitori servlet utilizzano classloader distinti per ogni servlet, quindi se due servlet accedono a un singleton, avranno ciascuno la propria istanza.

In quarto luogo, se ClassicSingletonimplementa l' java.io.Serializableinterfaccia, le istanze della classe possono essere serializzate e deserializzate. Tuttavia, se serializzi un oggetto singleton e successivamente deserializzi quell'oggetto più di una volta, avrai più istanze singleton.

Infine, e forse la cosa più importante, la ClassicSingletonclasse dell'Esempio 1 non è thread-safe. Se due thread, li chiameremo Thread 1 e Thread 2, chiamano ClassicSingleton.getInstance()contemporaneamente, ClassicSingletonpossono essere create due istanze se il Thread 1 viene interrotto subito dopo essere entrato nel ifblocco e il controllo viene successivamente dato al Thread 2.

Come puoi vedere dalla discussione precedente, sebbene il pattern Singleton sia uno dei modelli di progettazione più semplici, implementarlo in Java è tutt'altro che semplice. Il resto di questo articolo affronta considerazioni specifiche di Java per il pattern Singleton, ma prima facciamo una breve deviazione per vedere come puoi testare le tue classi Singleton.

Prova i singleton

Nel resto di questo articolo, utilizzo JUnit insieme a log4j per testare le classi singleton. Se non hai familiarità con JUnit o log4j, vedi Risorse.

L'Esempio 2 elenca un test case JUnit che verifica il singleton dell'Esempio 1:

Esempio 2. Un caso di test singleton

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) { super(name); } public void setUp() { logger.info("getting singleton..."); sone = ClassicSingleton.getInstance(); logger.info("...got singleton: " + sone); logger.info("getting singleton..."); stwo = ClassicSingleton.getInstance(); logger.info("...got singleton: " + stwo); } public void testUnique() { logger.info("checking singletons for equality"); Assert.assertEquals(true, sone == stwo); } }

Il test case dell'Esempio 2 invoca ClassicSingleton.getInstance()due volte e memorizza i riferimenti restituiti nelle variabili membro. Il testUnique()metodo verifica che i riferimenti siano identici. L'esempio 3 mostra l'output del test case:

Esempio 3. Output dello scenario di test

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) compile: run-test-text: [java] .INFO main: getting singleton... [java] INFO main: created singleton: [email protected] [java] INFO main: ...got singleton: [email protected] [java] INFO main: getting singleton... [java] INFO main: ...got singleton: [email protected] [java] INFO main: checking singletons for equality [java] Time: 0.032 [java] OK (1 test)

Come mostra l'elenco precedente, il semplice test dell'Esempio 2 viene superato a pieni voti: i due riferimenti singoli ottenuti con ClassicSingleton.getInstance()sono effettivamente identici; tuttavia, quei riferimenti sono stati ottenuti in un unico thread. La sezione successiva sottopone a stress test la nostra classe singleton con più thread.

Considerazioni sul multithreading

Il ClassicSingleton.getInstance()metodo dell'esempio 1 non è thread-safe a causa del codice seguente:

1: if(instance == null) { 2: instance = new Singleton(); 3: }

If a thread is preempted at Line 2 before the assignment is made, the instance member variable will still be null, and another thread can subsequently enter the if block. In that case, two distinct singleton instances will be created. Unfortunately, that scenario rarely occurs and is therefore difficult to produce during testing. To illustrate this thread Russian roulette, I've forced the issue by reimplementing Example 1's class. Example 4 shows the revised singleton class:

Example 4. Stack the deck

import org.apache.log4j.Logger; public class Singleton { private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger(); private static boolean firstThread = true; protected Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; } private static void simulateRandomActivity() { try { if(firstThread) { firstThread = false; logger.info("sleeping..."); // This nap should give the second thread enough time // to get by the first thread.Thread.currentThread().sleep(50); } } catch(InterruptedException ex) { logger.warn("Sleep interrupted"); } } }

Example 4's singleton resembles Example 1's class, except the singleton in the preceding listing stacks the deck to force a multithreading error. The first time the getInstance() method is called, the thread that invoked the method sleeps for 50 milliseconds, which gives another thread time to call getInstance() and create a new singleton instance. When the sleeping thread awakes, it also creates a new singleton instance, and we have two singleton instances. Although Example 4's class is contrived, it stimulates the real-world situation where the first thread that calls getInstance() gets preempted.

Example 5 tests Example 4's singleton:

Example 5. A test that fails

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private static Logger logger = Logger.getRootLogger(); private static Singleton singleton = null; public SingletonTest(String name) { super(name); } public void setUp() { singleton = null; } public void testUnique() throws InterruptedException { // Both threads call Singleton.getInstance(). Thread threadOne = new Thread(new SingletonTestRunnable()), threadTwo = new Thread(new SingletonTestRunnable()); threadOne.start();threadTwo.start(); threadOne.join(); threadTwo.join(); } private static class SingletonTestRunnable implements Runnable { public void run() { // Get a reference to the singleton. Singleton s = Singleton.getInstance(); // Protect singleton member variable from // multithreaded access. synchronized(SingletonTest.class) { if(singleton == null) // If local reference is null... singleton = s; // ...set it to the singleton } // Local reference must be equal to the one and // only instance of Singleton; otherwise, we have two // Singleton instances. Assert.assertEquals(true, s == singleton); } } }

Example 5's test case creates two threads, starts each one, and waits for them to finish. The test case maintains a static reference to a singleton instance, and each thread calls Singleton.getInstance(). If the static member variable has not been set, the first thread sets it to the singleton obtained with the call to getInstance(), and the static member variable is compared to the local variable for equality.

Ecco cosa succede quando viene eseguito il test case: il primo thread chiama getInstance(), entra nel ifblocco e dorme. Successivamente, anche il secondo thread chiama getInstance()e crea un'istanza singleton. Il secondo thread imposta quindi la variabile membro statica sull'istanza creata. Il secondo thread controlla l'uguaglianza della variabile membro statica e della copia locale e il test viene superato. Quando il primo thread si attiva, crea anche un'istanza singleton, ma quel thread non imposta la variabile membro statica (perché il secondo thread l'ha già impostata), quindi la variabile statica e la variabile locale non sono sincronizzate e il test perché l'uguaglianza fallisce. L'Esempio 6 elenca l'output del test case dell'Esempio 5: