Как да се ориентирате в измамно простия модел на Singleton

Моделът Singleton е измамно прост, дори и особено за разработчиците на Java. В тази класическа статия за JavaWorld Дейвид Гири демонстрира как разработчиците на Java прилагат единични модели, с примери за кодове за многопоточност, зареждане на класове и сериализация, използвайки шаблона Singleton. Той завършва с поглед върху внедряването на сингълтън регистри, за да се уточнят сингълтони по време на изпълнение.

Понякога е подходящо да имате точно един екземпляр на клас: мениджърите на прозорци, буферите за печат и файловите системи са прототипични примери. Обикновено тези видове обекти - известни като единични - са достъпни от различни обекти в цялата софтуерна система и следователно изискват глобална точка за достъп. Разбира се, точно когато сте сигурни, че никога няма да имате нужда от повече от един екземпляр, добре е да промените решението си.

Дизайнерският модел на Singleton адресира всички тези проблеми. С дизайна на Singleton можете да:

  • Уверете се, че е създаден само един екземпляр на клас
  • Осигурете глобална точка за достъп до обекта
  • Разрешаване на множество екземпляри в бъдеще, без да се засягат клиенти на единичен клас

Въпреки че дизайнът на Singleton - както се вижда по-долу от фигурата по-долу - е един от най-простите дизайнерски модели, той представлява редица подводни камъни за невнимателния разработчик на Java. Тази статия разглежда модела на дизайн на Singleton и разглежда тези подводни камъни.

Повече за моделите за дизайн на Java

Можете да прочетете всички колони на Java Design Patterns на Дейвид Гири или да видите списък с най -новите статии на JavaWorld за моделите на Java дизайн. Вижте „ Дизайнерски модели, голямата картина “ за дискусия относно плюсовете и минусите на използването на моделите „Бандата на четирите“. Искам още? Вземете бюлетина за Enterprise Java, доставен във вашата пощенска кутия.

Моделът Singleton

В Design Patterns: Elements of Reusable Object-Oriented Software , Gang of Four описва модела Singleton по следния начин:

Уверете се, че клас има само един екземпляр и осигурете глобална точка за достъп до него.

Фигурата по-долу илюстрира схемата на класа на шаблона за дизайн на Singleton.

Както можете да видите, няма много много в дизайна на Singleton. Единичните поддържат статична препратка към единствения единичен екземпляр и връщат препратка към този екземпляр от статичен instance()метод.

Пример 1 показва класическо изпълнение на модел за дизайн на Singleton:

Пример 1. Класическият единичен

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

Сингълтонът, реализиран в Пример 1, е лесен за разбиране. В ClassicSingletonкласа поддържа статичното позоваване на свободна сек съд и се връща че справка от статичен getInstance()метод.

Има няколко интересни точки относно ClassicSingletonкласа. Първо, ClassicSingletonизползва техника, известна като мързелива инстанция, за да създаде сингълтона; в резултат единичният екземпляр не се създава, докато getInstance()методът не бъде извикан за първи път. Тази техника гарантира, че единични екземпляри се създават само когато е необходимо.

Второ, забележете, че ClassicSingletonреализира защитен конструктор, така че клиентите да не могат да създават ClassicSingletonекземпляри; обаче може да се изненадате да откриете, че следният код е напълно легален:

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

Как може класът в предходния фрагмент на код - който не се разширява - да ClassicSingletonсъздаде ClassicSingletonекземпляр, ако ClassicSingletonконструкторът е защитен? Отговорът е, че защитени конструктори могат да бъдат извиквани от подкласове и от други класове в същия пакет . Тъй като ClassicSingletonи SingletonInstantiatorса в същия пакет (пакета по подразбиране), SingletonInstantiator()методите могат да създават ClassicSingletonекземпляри. Тази дилема има две решения: Можете да направите ClassicSingletonконструктора частен, така че само ClassicSingleton()методите да го извикват; това средство обаче ClassicSingletonне може да бъде подкласирано. Понякога това е желателно решение; ако е така, добре е да декларирате вашия сингълтон класfinal, което прави това намерение изрично и позволява на компилатора да приложи оптимизации на производителността. Другото решение е да поставите вашия сингълтън клас в изричен пакет, така че класовете в други пакети (включително пакета по подразбиране) не могат да създават екземпляри на единични единици.

Трети интересен момент за ClassicSingleton: възможно е да има множество екземпляри на единични, ако класовете, заредени от различни товарачи на класове, имат достъп до единичен. Този сценарий не е толкова замислен; например някои контейнери за сървлети използват отделни зареждачи на класове за всеки сървлет, така че ако два сървлета имат достъп до единичен, всеки от тях ще има свой собствен екземпляр.

Четвърто, ако ClassicSingletonреализира java.io.Serializableинтерфейса, екземплярите на класа могат да бъдат сериализирани и десериализирани. Ако обаче сериализирате единичен обект и впоследствие десериализирате този обект повече от веднъж, ще имате множество единични екземпляри.

И накрая и може би най-важното, ClassicSingletonкласът на Пример 1 не е безопасен за нишките. Ако две нишки - ние ще ги наречем Thread 1 и Thread 2 - се обадят ClassicSingleton.getInstance()едновременно, ClassicSingletonмогат да бъдат създадени два екземпляра, ако Thread 1 бъде изпреварен непосредствено след влизането му в ifблока и впоследствие контролът е даден на Thread 2.

Както можете да видите от предходната дискусия, въпреки че моделът Singleton е един от най-простите дизайнерски модели, прилагането му в Java е всичко друго, но не и просто. Останалата част от тази статия разглежда специфичните за Java съображения за модела Singleton, но първо нека направим кратък обход, за да видим как можете да тествате вашите сингълтон класове.

Тествайте единични

В останалата част на тази статия използвам JUnit заедно с log4j за тестване на сингълтон класове. Ако не сте запознати с JUnit или log4j, вижте Ресурси.

Пример 2 изброява JUnit тестов случай, който тества сингълтона на Пример 1:

Пример 2. Единичен тестов случай

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

Тестовият случай на Пример 2 извиква ClassicSingleton.getInstance()два пъти и съхранява върнатите препратки в променливи-членове. На testUnique()проверки метод да се види, че препратките са идентични. Пример 3 показва изхода на тестовия случай:

Пример 3. Изход на тестовия случай

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)

Както илюстрира предходният списък, простият тест на Пример 2 преминава с летящи цветове - получените две единични препратки ClassicSingleton.getInstance()наистина са идентични; тези препратки обаче бяха получени в една нишка. Следващият раздел подлага на стрес тестове нашия единичен клас с множество нишки.

Съображения за многопоточност

ClassicSingleton.getInstance()Методът на пример 1 не е безопасен за нишки поради следния код:

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.

Ето какво се случва, когато се изпълни тестовият случай: Първата нишка извиква getInstance(), влиза в ifблока и спи. Впоследствие втората нишка също извиква getInstance()и създава единичен екземпляр. След това втората нишка задава статичната променлива на член на екземпляра, който е създала. Втората нишка проверява за равнопоставеност статичната променлива на член и локалното копие и тестът преминава. Когато първата нишка се събуди, тя също създава единичен екземпляр, но тази нишка не задава статичната променлива на член (тъй като втората нишка вече я е задала), така че статичната променлива и локалната променлива не са синхронизирани и тестът за равенството се проваля. Пример 6 изброява изходния пример на пример 5: