Jak poruszać się po zwodniczo prostym wzorcu Singleton

Wzorzec Singleton jest zwodniczo prosty, nawet i szczególnie dla programistów Java. W tym klasycznym artykule JavaWorld David Geary demonstruje, w jaki sposób programiści Java implementują singletony, z przykładami kodu dla wielowątkowości, programów ładujących klasy i serializacji przy użyciu wzorca Singleton. Na zakończenie zwraca uwagę na implementację rejestrów pojedynczych w celu określenia pojedynczych rejestrów w czasie wykonywania.

Czasami dobrze jest mieć dokładnie jedną instancję klasy: menedżery okien, bufory wydruku i systemy plików są prototypowymi przykładami. Zazwyczaj do tego typu obiektów - znanych jako pojedyncze - mają dostęp różne obiekty w całym systemie oprogramowania i dlatego wymagają globalnego punktu dostępu. Oczywiście, gdy jesteś pewien, że nigdy nie będziesz potrzebować więcej niż jednej instancji, dobrze jest założyć, że zmienisz zdanie.

Wzorzec projektowy Singleton rozwiązuje wszystkie te problemy. Dzięki wzorcowi projektowemu Singleton możesz:

  • Upewnij się, że została utworzona tylko jedna instancja klasy
  • Zapewnij globalny punkt dostępu do obiektu
  • Zezwalaj na wiele wystąpień w przyszłości bez wpływu na klientów klasy pojedynczej

Chociaż wzorzec projektowy Singleton - jak widać na poniższym rysunku - jest jednym z najprostszych wzorców projektowych, stwarza wiele pułapek dla nieostrożnego programisty Java. W tym artykule omówiono wzorzec projektowy Singleton i rozwiązano te pułapki.

Więcej o wzorcach projektowych Java

Możesz przeczytać wszystkie kolumny David Geary's Java Design Patterns lub wyświetlić listę najnowszych artykułów JavaWorld na temat wzorców projektowych Java. Zobacz „ Wzorce projektowe, szerszy obraz ”, aby zapoznać się z zaletami i wadami korzystania z wzorców Gang of Four. Chcieć więcej? Otrzymuj biuletyn Enterprise Java dostarczony do swojej skrzynki odbiorczej.

Wzorzec Singleton

We wzorcach projektowych: elementy oprogramowania obiektowego wielokrotnego użytku , Gang of Four opisuje wzorzec Singleton w następujący sposób:

Upewnij się, że klasa ma tylko jedną instancję i zapewnij globalny punkt dostępu do niej.

Poniższy rysunek ilustruje diagram klas wzorca projektowego Singleton.

Jak widać, we wzorcu projektowym Singleton nie ma wiele. Singletony utrzymują statyczne odwołanie do jedynej pojedynczej instancji i zwracają odwołanie do tej instancji z instance()metody statycznej .

Przykład 1 przedstawia klasyczną implementację wzorca projektowego Singleton:

Przykład 1. Klasyczny 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; } }

Singleton zaimplementowany w przykładzie 1 jest łatwy do zrozumienia. ClassicSingletonKlasy utrzymuje statyczną odniesienie do samodzielnego przykład jednoelementowy i zwraca odniesienia w statycznym getInstance()metody.

Jest kilka interesujących punktów dotyczących tej ClassicSingletonklasy. Po pierwsze, ClassicSingletonwykorzystuje technikę znaną jako leniwa instancja do utworzenia singletona; w rezultacie pojedyncza instancja nie jest tworzona, dopóki getInstance()metoda nie zostanie wywołana po raz pierwszy. Ta technika zapewnia, że ​​pojedyncze wystąpienia są tworzone tylko w razie potrzeby.

Po drugie, należy zauważyć, że ClassicSingletonimplementuje chroniony konstruktor, dzięki czemu klienci nie mogą tworzyć ClassicSingletoninstancji; jednak możesz być zaskoczony, gdy odkryjesz, że następujący kod jest całkowicie legalny:

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

W jaki sposób klasa w poprzednim fragmencie kodu - który się nie rozszerza ClassicSingleton- może utworzyć ClassicSingletoninstancję, jeśli ClassicSingletonkonstruktor jest chroniony? Odpowiedź jest taka, że ​​chronione konstruktory mogą być wywoływane przez podklasy i inne klasy w tym samym pakiecie . Ponieważ ClassicSingletoni SingletonInstantiatorznajdują się w tym samym pakiecie (pakiet domyślny), SingletonInstantiator()metody mogą tworzyć ClassicSingletonwystąpienia. Ten dylemat ma dwa rozwiązania: Możesz ustawić ClassicSingletonkonstruktor jako prywatny, tak aby ClassicSingleton()wywoływały go tylko metody; jednak oznacza to, że ClassicSingletonnie można ich podklasy. Czasami jest to pożądane rozwiązanie; jeśli tak, dobrze jest zadeklarować swoją klasę singletonfinal, co czyni ten zamiar jawnym i umożliwia kompilatorowi stosowanie optymalizacji wydajności. Innym rozwiązaniem jest umieszczenie klasy pojedynczej w jawnym pakiecie, aby klasy w innych pakietach (w tym pakiet domyślny) nie mogły tworzyć instancji pojedynczych.

Trzecia interesująca kwestia dotycząca ClassicSingleton: możliwe jest posiadanie wielu pojedynczych wystąpień, jeśli klasy ładowane przez różne programy ładujące klasy mają dostęp do singletona. Ten scenariusz nie jest tak daleko idący; na przykład niektóre kontenery serwletów używają odrębnych programów ładujących klasy dla każdego serwletu, więc jeśli dwa serwlety mają dostęp do pojedynczego, każdy z nich będzie miał własną instancję.

Po czwarte, jeśli ClassicSingletonimplementuje java.io.Serializableinterfejs, wystąpienia klasy mogą być serializowane i deserializowane. Jeśli jednak serializujesz pojedynczy obiekt, a następnie deserializujesz ten obiekt więcej niż raz, będziesz mieć wiele pojedynczych wystąpień.

Wreszcie, i być może najważniejsze, ClassicSingletonklasa z przykładu 1 nie jest bezpieczna dla wątków. Jeśli dwa wątki - nazwiemy je Wątkiem 1 i Wątkiem 2 - wywołują ClassicSingleton.getInstance()w tym samym czasie, ClassicSingletonmożna utworzyć dwa wystąpienia, jeśli wątek 1 zostanie wywłaszczony tuż po wejściu do ifbloku, a kontrola zostanie następnie przekazana wątkowi 2.

Jak widać z poprzedniej dyskusji, chociaż wzorzec Singleton jest jednym z najprostszych wzorców projektowych, implementacja go w Javie wcale nie jest prosta. W pozostałej części tego artykułu omówiono kwestie specyficzne dla języka Java dotyczące wzorca Singleton, ale najpierw przyjrzyjmy się krótkiemu objazdowi, aby zobaczyć, jak można przetestować klasy singleton.

Testuj singletony

W pozostałej części tego artykułu używam JUnit w połączeniu z log4j do testowania klas pojedynczych. Jeśli nie znasz JUnit lub log4j, zobacz Zasoby.

Przykład 2 przedstawia przypadek testowy JUnit, który testuje singleton z przykładu 1:

Przykład 2. Pojedynczy przypadek testowy

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

Przypadek testowy z przykładu 2 wywołuje ClassicSingleton.getInstance()dwukrotnie i przechowuje zwrócone odwołania w zmiennych składowych. Te testUnique()kontrole metoda, aby zobaczyć, że odwołania są identyczne. Przykład 3 pokazuje, że wynik testu:

Przykład 3. Wyjście testu

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)

Jak ilustruje poprzednia lista, proste testy z przykładu 2 przebiegają celująco - dwie pojedyncze referencje otrzymane za pomocą ClassicSingleton.getInstance()są rzeczywiście identyczne; jednak te odniesienia zostały uzyskane w jednym wątku. Następna sekcja przeprowadza testy obciążeniowe naszej klasy pojedynczej z wieloma wątkami.

Zagadnienia dotyczące wielowątkowości

ClassicSingleton.getInstance()Metoda przykładu 1 nie jest bezpieczna wątkowo z powodu następującego kodu:

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.

Oto, co się dzieje, gdy uruchamia się przypadek testowy: pierwszy wątek wywołuje getInstance(), wchodzi do ifbloku i śpi. Następnie drugi wątek również wywołuje getInstance()i tworzy pojedyncze wystąpienie. Drugi wątek następnie ustawia statyczną zmienną składową na utworzoną instancję. Drugi wątek sprawdza statyczną zmienną składową i kopię lokalną pod kątem równości, a test kończy się pomyślnie. Gdy pierwszy wątek budzi się, tworzy również pojedyncze wystąpienie, ale ten wątek nie ustawia statycznej zmiennej składowej (ponieważ drugi wątek już ją ustawił), więc zmienna statyczna i zmienna lokalna nie są zsynchronizowane, a test bo równość zawodzi. Przykład 6 przedstawia wyniki testu z przykładu 5: