So navigieren Sie durch das täuschend einfache Singleton-Muster

Das Singleton-Muster ist selbst für Java-Entwickler täuschend einfach. In diesem klassischen JavaWorld- Artikel demonstriert David Geary, wie Java-Entwickler Singletons implementieren, mit Codebeispielen für Multithreading, Klassenladeprogramme und Serialisierung unter Verwendung des Singleton-Musters. Er schließt mit einem Blick auf die Implementierung von Singleton-Registern, um Singletons zur Laufzeit anzugeben.

Manchmal ist es angebracht, genau eine Instanz einer Klasse zu haben: Fenstermanager, Druckerspooler und Dateisysteme sind prototypische Beispiele. In der Regel wird auf diese Objekttypen - sogenannte Singletons - von unterschiedlichen Objekten in einem Softwaresystem zugegriffen, und daher ist ein globaler Zugriffspunkt erforderlich. Nur wenn Sie sicher sind, dass Sie nie mehr als eine Instanz benötigen, ist es eine gute Wette, dass Sie Ihre Meinung ändern.

Das Singleton-Entwurfsmuster berücksichtigt all diese Probleme. Mit dem Singleton-Entwurfsmuster können Sie:

  • Stellen Sie sicher, dass nur eine Instanz einer Klasse erstellt wird
  • Stellen Sie einen globalen Zugriffspunkt auf das Objekt bereit
  • Erlauben Sie in Zukunft mehrere Instanzen, ohne die Clients einer Singleton-Klasse zu beeinträchtigen

Obwohl das Singleton-Entwurfsmuster - wie in der folgenden Abbildung dargestellt - eines der einfachsten Entwurfsmuster ist, bietet es dem unachtsamen Java-Entwickler eine Reihe von Fallstricken. Dieser Artikel beschreibt das Singleton-Entwurfsmuster und geht auf diese Fallstricke ein.

Weitere Informationen zu Java-Entwurfsmustern

Sie können alle Spalten zu Java Design Patterns von David Geary lesen oder eine Liste der neuesten Artikel von JavaWorld zu Java Design Patterns anzeigen . Unter " Entwurfsmuster, das große Ganze " finden Sie eine Diskussion über die Vor- und Nachteile der Verwendung der Viererbande-Muster. Mehr wollen? Holen Sie sich den Enterprise Java-Newsletter in Ihren Posteingang.

Das Singleton-Muster

In Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software beschreibt die Viererbande das Singleton-Muster folgendermaßen:

Stellen Sie sicher, dass eine Klasse nur eine Instanz hat, und stellen Sie einen globalen Zugriffspunkt darauf bereit.

Die folgende Abbildung zeigt das Klassendiagramm des Singleton-Entwurfsmusters.

Wie Sie sehen können, enthält das Singleton-Entwurfsmuster nicht viel. Singletons behalten einen statischen Verweis auf die einzige Singleton-Instanz bei und geben einen Verweis auf diese Instanz von einer statischen instance()Methode zurück.

Beispiel 1 zeigt eine klassische Implementierung eines Singleton-Entwurfsmusters:

Beispiel 1. Der klassische 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; } }

Der in Beispiel 1 implementierte Singleton ist leicht zu verstehen. Die ClassicSingletonKlasse verwaltet eine statische Referenz auf die einzelne Singleton-Instanz und gibt diese Referenz von der statischen getInstance()Methode zurück.

Es gibt einige interessante Punkte in Bezug auf die ClassicSingletonKlasse. Erstens ClassicSingletonsetzt eine Technik bekannt als faul Instanziierung die Singleton zu schaffen; Infolgedessen wird die Singleton-Instanz erst erstellt, wenn die getInstance()Methode zum ersten Mal aufgerufen wird. Diese Technik stellt sicher, dass Singleton-Instanzen nur bei Bedarf erstellt werden.

Beachten Sie zweitens, dass ClassicSingletonein geschützter Konstruktor implementiert wird, sodass Clients keine ClassicSingletonInstanzen instanziieren können . Es kann Sie jedoch überraschen, dass der folgende Code vollkommen legal ist:

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

Wie kann die Klasse im vorhergehenden Codefragment - die nicht erweitert wird ClassicSingleton- eine ClassicSingletonInstanz erstellen, wenn der ClassicSingletonKonstruktor geschützt ist? Die Antwort lautet, dass geschützte Konstruktoren von Unterklassen und anderen Klassen im selben Paket aufgerufen werden können . Da ClassicSingletonund SingletonInstantiatorsich im selben Paket (dem Standardpaket) befinden, können SingletonInstantiator()Methoden ClassicSingletonInstanzen erstellen . Dieses Dilemma hat zwei Lösungen: Sie können den ClassicSingletonKonstruktor privat machen, sodass nur ClassicSingleton()Methoden ihn aufrufen. Dies bedeutet jedoch, dass ClassicSingletonkeine Unterklassen erstellt werden können. Manchmal ist das eine wünschenswerte Lösung; In diesem Fall ist es eine gute Idee, Ihre Singleton-Klasse zu deklarierenfinalDies macht diese Absicht explizit und ermöglicht es dem Compiler, Leistungsoptimierungen anzuwenden. Die andere Lösung besteht darin, Ihre Singleton-Klasse in ein explizites Paket zu stellen, sodass Klassen in anderen Paketen (einschließlich des Standardpakets) keine Singleton-Instanzen instanziieren können.

Ein dritter interessanter Punkt ClassicSingleton: Es ist möglich, mehrere Singleton-Instanzen zu haben, wenn Klassen, die von verschiedenen Klassenladeprogrammen geladen wurden, auf einen Singleton zugreifen. Dieses Szenario ist nicht so weit hergeholt; Beispielsweise verwenden einige Servlet-Container unterschiedliche Klassenladeprogramme für jedes Servlet. Wenn also zwei Servlets auf einen Singleton zugreifen, haben sie jeweils eine eigene Instanz.

Viertens können, wenn ClassicSingletondie java.io.SerializableSchnittstelle implementiert wird, die Instanzen der Klasse serialisiert und deserialisiert werden. Wenn Sie jedoch ein Singleton-Objekt serialisieren und dieses Objekt anschließend mehrmals deserialisieren, verfügen Sie über mehrere Singleton-Instanzen.

Schließlich und vielleicht am wichtigsten ist die ClassicSingletonKlasse von Beispiel 1 nicht threadsicher. Wenn zwei Threads - wir nennen sie Thread 1 und Thread 2 - gleichzeitig aufgerufen ClassicSingleton.getInstance()werden, können zwei ClassicSingletonInstanzen erstellt werden, wenn Thread 1 unmittelbar nach dem Eintritt in den ifBlock vorbelegt wird und anschließend die Steuerung an Thread 2 übergeben wird.

Wie Sie aus der vorhergehenden Diskussion sehen können, ist die Implementierung in Java alles andere als einfach, obwohl das Singleton-Muster eines der einfachsten Entwurfsmuster ist. Der Rest dieses Artikels befasst sich mit Java-spezifischen Überlegungen für das Singleton-Muster. Machen wir jedoch zunächst einen kurzen Umweg, um zu sehen, wie Sie Ihre Singleton-Klassen testen können.

Testen Sie Singletons

Im weiteren Verlauf dieses Artikels verwende ich JUnit zusammen mit log4j, um Singleton-Klassen zu testen. Wenn Sie mit JUnit oder log4j nicht vertraut sind, lesen Sie Ressourcen.

Beispiel 2 listet einen JUnit-Testfall auf, der den Singleton von Beispiel 1 testet:

Beispiel 2. Ein 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); } }

Der Testfall von Beispiel 2 wird ClassicSingleton.getInstance()zweimal aufgerufen und die zurückgegebenen Referenzen in Mitgliedsvariablen gespeichert. Die testUnique()Methode prüft, ob die Referenzen identisch sind. Beispiel 3 zeigt die Testfallausgabe:

Beispiel 3. Testfallausgabe

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)

Wie die vorstehende Auflistung zeigt, besteht der einfache Test von Beispiel 2 mit Bravour - die beiden mit erhaltenen Singleton-Referenzen ClassicSingleton.getInstance()sind tatsächlich identisch; Diese Referenzen wurden jedoch in einem einzigen Thread erhalten. Im nächsten Abschnitt wird unsere Singleton-Klasse mit mehreren Threads einem Stresstest unterzogen.

Multithreading-Überlegungen

Die ClassicSingleton.getInstance()Methode von Beispiel 1 ist aufgrund des folgenden Codes nicht threadsicher:

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.

Folgendes passiert, wenn der Testfall ausgeführt wird: Der erste Thread ruft auf getInstance(), tritt in den ifBlock ein und schläft. Anschließend ruft der zweite Thread auch getInstance()eine Singleton-Instanz auf und erstellt sie. Der zweite Thread setzt dann die statische Elementvariable auf die von ihm erstellte Instanz. Der zweite Thread überprüft die statische Elementvariable und die lokale Kopie auf Gleichheit, und der Test besteht. Wenn der erste Thread aktiviert wird, wird auch eine Singleton-Instanz erstellt. Dieser Thread legt jedoch nicht die statische Elementvariable fest (da der zweite Thread sie bereits festgelegt hat), sodass die statische Variable und die lokale Variable nicht synchron sind, und der Test denn Gleichheit scheitert. Beispiel 6 listet die Testfallausgabe von Beispiel 5 auf: