Hoe je door het bedrieglijk eenvoudige Singleton-patroon navigeert

Het Singleton-patroon is bedrieglijk eenvoudig, zelfs en vooral voor Java-ontwikkelaars. In dit klassieke JavaWorld- artikel laat David Geary zien hoe Java-ontwikkelaars singletons implementeren, met codevoorbeelden voor multithreading, classloaders en serialisatie met behulp van het Singleton-patroon. Hij besluit met een blik op het implementeren van singleton-registers om singletons tijdens runtime te specificeren.

Soms is het gepast om precies één instantie van een klasse te hebben: vensterbeheerders, print spoolers en bestandssystemen zijn prototypische voorbeelden. Dit soort objecten, bekend als singletons, worden doorgaans benaderd door ongelijksoortige objecten in een softwaresysteem en vereisen daarom een ​​globaal toegangspunt. Als u er zeker van bent dat u nooit meer dan één exemplaar nodig heeft, is het natuurlijk een goede gok dat u van gedachten verandert.

Het Singleton-ontwerppatroon lost al deze zorgen op. Met het Singleton-ontwerppatroon kunt u:

  • Zorg ervoor dat er slechts één instantie van een klasse wordt gemaakt
  • Zorg voor een globaal toegangspunt tot het object
  • Sta meerdere instanties in de toekomst toe zonder de clients van een singleton-klasse te beïnvloeden

Hoewel het Singleton-ontwerppatroon - zoals hieronder blijkt uit de onderstaande afbeelding - een van de eenvoudigste ontwerppatronen is, biedt het een aantal valkuilen voor de onoplettende Java-ontwikkelaar. Dit artikel bespreekt het Singleton-ontwerppatroon en behandelt die valkuilen.

Meer over Java-ontwerppatronen

U kunt alle kolommen van David Geary met Java Design Patterns lezen , of een lijst met de meest recente artikelen van JavaWorld over Java-ontwerppatronen bekijken. Zie " Design patterns, the big picture " voor een discussie over de voor- en nadelen van het gebruik van de Gang of Four-patronen. Meer willen? Ontvang de Enterprise Java-nieuwsbrief in uw inbox.

Het Singleton-patroon

In Design Patterns: Elements of Reusable Object-Oriented Software beschrijft de Gang of Four het Singleton-patroon als volgt:

Zorg ervoor dat een klasse slechts één instantie heeft en geef er een globaal toegangspunt voor.

De onderstaande afbeelding illustreert het klassendiagram van het Singleton-ontwerppatroon.

Zoals je kunt zien, is er niet veel aan het Singleton-ontwerppatroon. Singletons behouden een statische verwijzing naar de enige singleton-instantie en retourneren een verwijzing naar die instantie vanuit een statische instance()methode.

Voorbeeld 1 toont een klassieke implementatie van Singleton-ontwerppatronen:

Voorbeeld 1. De klassieke 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; } }

De singleton geïmplementeerd in Voorbeeld 1 is gemakkelijk te begrijpen. De ClassicSingletonklasse houdt een statische verwijzing bij naar de eenzame singleton-instantie en retourneert die verwijzing van de statische getInstance()methode.

Er zijn verschillende interessante punten met betrekking tot de ClassicSingletonklas. Ten eerste, ClassicSingletongebruikt een techniek die bekend staat als luie instantiatie om de singleton te creëren; als resultaat wordt de singleton-instantie pas gemaakt als de getInstance()methode voor de eerste keer wordt aangeroepen. Deze techniek zorgt ervoor dat singleton-instances alleen worden gemaakt als dat nodig is.

Ten tweede, merk op dat ClassicSingletoneen beschermde constructor wordt geïmplementeerd, zodat clients geen instanties kunnen ClassicSingletoninstantiëren; het zal je misschien verbazen te ontdekken dat de volgende code volkomen legaal is:

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

Hoe kan de klasse in het voorgaande codefragment - dat niet uitbreidt ClassicSingleton- een ClassicSingletoninstantie maken als de ClassicSingletonconstructor is beveiligd? Het antwoord is dat beschermde constructors kunnen worden aangeroepen door subklassen en door andere klassen in hetzelfde pakket . Omdat ClassicSingletonen SingletonInstantiatorzich in hetzelfde pakket bevinden (het standaardpakket), kunnen SingletonInstantiator()methoden ClassicSingletoninstanties maken. Dit dilemma heeft twee oplossingen: je kunt de ClassicSingletonconstructor privé maken, zodat alleen ClassicSingleton()methoden hem aanroepen; dat middel ClassicSingletonkan echter niet worden onderverdeeld. Soms is dat een wenselijke oplossing; als dat het geval is, is het een goed idee om uw singleton-klasse aan te gevenfinal, wat die intentie expliciet maakt en de compiler in staat stelt prestatie-optimalisaties toe te passen. De andere oplossing is om je singleton-klasse in een expliciet pakket te plaatsen, zodat klassen in andere pakketten (inclusief het standaardpakket) geen singleton-instanties kunnen instantiëren.

Een derde interessant punt over ClassicSingleton: het is mogelijk om meerdere singleton instances te hebben als klassen geladen door verschillende classloaders een singleton benaderen. Dat scenario is niet zo vergezocht; Sommige servletcontainers gebruiken bijvoorbeeld verschillende classloaders voor elke servlet, dus als twee servlets toegang hebben tot een singleton, hebben ze elk hun eigen instantie.

Ten vierde, als ClassicSingletonde java.io.Serializableinterface wordt geïmplementeerd , kunnen de instanties van de klasse worden geserialiseerd en gedeserialiseerd. Als u echter een singleton-object serialiseert en dat object vervolgens meer dan eens deserialiseert, hebt u meerdere singleton-instanties.

Ten slotte, en misschien wel het belangrijkste, is de ClassicSingletonklasse van Voorbeeld 1 niet thread-safe. Als twee threads - we noemen ze Thread 1 en Thread 2 - tegelijkertijd aanroepen ClassicSingleton.getInstance(), kunnen twee ClassicSingletoninstanties worden gemaakt als Thread 1 wordt gepreempt net nadat het het ifblok is binnengekomen en de controle vervolgens aan Thread 2 wordt gegeven.

Zoals je in de voorgaande discussie kunt zien, is het Singleton-patroon een van de eenvoudigste ontwerppatronen, maar het implementeren ervan in Java is allesbehalve eenvoudig. De rest van dit artikel behandelt Java-specifieke overwegingen voor het Singleton-patroon, maar laten we eerst een korte omweg maken om te zien hoe u uw singleton-klassen kunt testen.

Test singletons

In de rest van dit artikel gebruik ik JUnit in combinatie met log4j om singleton-klassen te testen. Zie bronnen als u niet bekend bent met JUnit of log4j.

Voorbeeld 2 geeft een lijst van een JUnit-testcase die de singleton van Voorbeeld 1 test:

Voorbeeld 2. Een singleton-testcase

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

De testcase van Voorbeeld 2 wordt ClassicSingleton.getInstance()tweemaal aangeroepen en slaat de geretourneerde verwijzingen op in lidvariabelen. De testUnique()methode controleert of de referenties identiek zijn. Voorbeeld 3 laat zien dat de uitvoer van een testcase:

Voorbeeld 3. Uitvoer van testgeval

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)

Zoals de voorgaande lijst illustreert, slaagt de eenvoudige test van Voorbeeld 2 met vlag en wimpel - de twee verkregen singletonreferenties ClassicSingleton.getInstance()zijn inderdaad identiek; die referenties werden echter in één thread verkregen. De volgende sectie test onze singleton-klasse met meerdere threads.

Overwegingen bij multithreading

De ClassicSingleton.getInstance()methode van voorbeeld 1 is niet thread-safe vanwege de volgende code:

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.

Dit is wat er gebeurt als de testcase wordt uitgevoerd: de eerste thread roept getInstance(), gaat het ifblok binnen en slaapt. Vervolgens roept de tweede thread ook getInstance()een singleton-instantie aan. De tweede thread stelt vervolgens de statische lidvariabele in op de instantie die deze heeft gemaakt. De tweede thread controleert de statische lidvariabele en de lokale kopie op gelijkheid, en de test slaagt. Wanneer de eerste thread ontwaakt, wordt ook een singleton-instantie gemaakt, maar die thread stelt de statische lidvariabele niet in (omdat de tweede thread deze al heeft ingesteld), dus de statische variabele en de lokale variabele zijn niet synchroon, en de test want gelijkheid mislukt. Voorbeeld 6 geeft een overzicht van de testcase-uitvoer van Voorbeeld 5: