Hur man navigerar i det bedrägligt enkla Singleton-mönstret

Singleton-mönstret är bedrägligt enkelt, jämnt och särskilt för Java-utvecklare. I den här klassiska JavaWorld- artikeln visar David Geary hur Java-utvecklare implementerar singletons, med kodexempel för multithreading, classloaders och serialisering med Singleton-mönstret. Han avslutar med en titt på implementering av singletonregister för att specificera singletons vid körning.

Ibland är det lämpligt att ha exakt en instans av en klass: fönsterhanterare, utskriftskylare och filsystem är prototypiska exempel. Vanligtvis nås dessa typer av objekt - så kallade singletons - av olika objekt i ett mjukvarusystem och kräver därför en global åtkomstpunkt. Naturligtvis, precis när du är säker på att du aldrig behöver mer än en instans, är det en bra insats att du ändrar dig.

Singleton-designmönstret hanterar alla dessa problem. Med designmönstret Singleton kan du:

  • Se till att endast en instans av en klass skapas
  • Ge en global åtkomstpunkt till objektet
  • Tillåt flera instanser i framtiden utan att påverka klienterna i en singleton-klass

Även om Singleton-designmönstret - som framgår nedan av figuren nedan - är ett av de enklaste designmönstren, presenterar det ett antal fallgropar för den oförsiktiga Java-utvecklaren. Den här artikeln diskuterar Singletons designmönster och behandlar de fallgropar.

Mer om Java-designmönster

Du kan läsa alla David Gearys Java Design Patterns-kolumner eller visa en lista över JavaWorlds senaste artiklar om Java-designmönster. Se " Designmönster, helheten " för en diskussion om fördelar och nackdelar med att använda Gang of Four-mönster. Vill ha mer? Få Enterprise Java-nyhetsbrevet levererat till din inkorg.

Singleton-mönstret

I Design Patterns: Elements of Reusable Object-Oriented Software , the Gang of Four beskriver Singleton-mönstret så här:

Se till att en klass bara har en instans och ge en global åtkomstpunkt till den.

Figuren nedan illustrerar Singletons klassdiagram för designmönster.

Som du kan se finns det inte så mycket med Singleton-designmönstret. Singletons upprätthåller en statisk referens till den enda singletoninstansen och returnerar en referens till den instansen från en statisk instance()metod.

Exempel 1 visar en klassisk Singleton-designmönsterimplementering:

Exempel 1. Det klassiska singletonet

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; } }

Singleton som implementerats i exempel 1 är lätt att förstå. Den ClassicSingletonklass bibehåller en statisk referens till den lone singleton instans och avkastning som referens från den statiska getInstance()metoden.

Det finns flera intressanta punkter angående ClassicSingletonklassen. För det första ClassicSingletonanvänder vi en teknik som kallas lat instans för att skapa singleton; som ett resultat skapas inte singleton-instansen förrän getInstance()metoden anropas för första gången. Denna teknik säkerställer att singleton-instanser skapas endast när det behövs.

För det andra, lägg märke till att ClassicSingletonen skyddad konstruktör implementeras så att klienter inte kan starta ClassicSingletoninstanser; dock kan du bli förvånad över att upptäcka att följande kod är helt laglig:

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

Hur kan klassen i föregående kodfragment - som inte sträcker sig - ClassicSingletonskapa en ClassicSingletoninstans om ClassicSingletonkonstruktören är skyddad? Svaret är att skyddade konstruktörer kan anropas av underklasser och av andra klasser i samma paket . Eftersom ClassicSingletonoch SingletonInstantiatorfinns i samma paket (standardpaketet) kan SingletonInstantiator()metoder skapa ClassicSingletoninstanser. Detta dilemma har två lösningar: Du kan göra ClassicSingletonkonstruktören privat så att endast ClassicSingleton()metoder kallar det; det betyder dock ClassicSingletoninte att det kan delklassificeras. Ibland är det en önskvärd lösning; i så fall är det en bra idé att förklara din singleton-klassfinal, vilket gör denna avsikt uttrycklig och gör att kompilatorn kan använda prestandaoptimeringar. Den andra lösningen är att placera din singleton-klass i ett explicit paket, så klasser i andra paket (inklusive standardpaketet) kan inte starta singleton-instanser.

En tredje intressant punkt om ClassicSingleton: det är möjligt att ha flera singleton-instanser om klasser laddade av olika klasslastare får åtkomst till en singleton. Det scenariot är inte så långt hämtat; till exempel använder vissa servletcontainrar distinkta klassladdare för varje servlet, så om två servlets har åtkomst till en singleton har de var och en sin egen instans.

För det fjärde, om ClassicSingletonimplementerar java.io.Serializablegränssnittet, kan klassens instanser serieiseras och deserialiseras. Men om du serierar ett singleton-objekt och därefter avserialiserar objektet mer än en gång kommer du att ha flera singleton-instanser.

Slutligen, och kanske viktigast, är exempel 1: s ClassicSingletonklass inte trådsäker. Om två trådar - vi kallar dem tråd 1 och tråd 2 - ringer ClassicSingleton.getInstance()samtidigt, kan två ClassicSingletoninstanser skapas om tråd 1 förhindras precis efter att den kommer in i ifblocket och kontroll därefter ges till tråd 2.

Som du kan se från föregående diskussion, även om Singleton-mönstret är ett av de enklaste designmönstren, är det allt annat än enkelt att implementera det i Java. Resten av den här artikeln behandlar Java-specifika överväganden för Singleton-mönstret, men låt oss först ta en kort omväg för att se hur du kan testa dina singletonklasser.

Testa singletons

Under resten av den här artikeln använder jag JUnit i konsert med log4j för att testa singleton-klasser. Om du inte känner till JUnit eller log4j, se Resurser.

Exempel 2 listar ett JUnit-testfall som testar exempel 1: s singleton:

Exempel 2. Ett singleton testfall

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); } }

Exempel 2: s testfall åberopar ClassicSingleton.getInstance()två gånger och lagrar de returnerade referenserna i medlemsvariabler. De testUnique()metodkontroller för att se att hänvisningarna är identiska. Exempel 3 visar att testfallets utdata:

Exempel 3. Testfallssignal

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)

Som den föregående listan illustrerar passerar exempel 2: s enkla test med flygande färger - de två singletonreferenser som erhållits med ClassicSingleton.getInstance()är verkligen identiska; emellertid erhölls dessa referenser i en enda tråd. Nästa avsnitt stresstestar vår singleton-klass med flera trådar.

Multithreading-överväganden

Exempel 1: s ClassicSingleton.getInstance()metod är inte trådsäker på grund av följande kod:

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.

Här är vad som händer när testfallet körs: Den första tråden ringer getInstance(), går in i ifblocket och sover. Därefter anropar den andra tråden getInstance()och skapar en singleton-instans. Den andra tråden ställer sedan in den statiska medlemsvariabeln till den instans den skapade. Den andra tråden kontrollerar den statiska medlemsvariabeln och den lokala kopian för jämlikhet, och testet passerar. När den första tråden vaknar skapar den också en singleton-instans, men den tråden ställer inte in den statiska medlemsvariabeln (eftersom den andra tråden redan har ställt in den), så den statiska variabeln och den lokala variabeln är ur synk och testet för jämställdhet misslyckas. Exempel 6 listar Exempel 5: s testfalloutput: