Programování

Jak se orientovat v klamně jednoduchém Singletonově vzoru

Vzor Singleton je klamně jednoduchý, rovnoměrný a zvláště pro vývojáře Java. V této klasice JavaWorld článek, David Geary ukazuje, jak vývojáři Java implementují singletony, s příklady kódu pro vícevláknové zpracování, načítání tříd a serializaci pomocí vzoru Singleton. Na závěr se podívá na implementaci singletonových registrů, aby za běhu určil singletony.

Někdy je vhodné mít právě jednu instanci třídy: prototypy příkladů jsou správci oken, zařazovací služby tisku a souborové systémy. K těmto typům objektů - známým jako singletony - se obvykle v rámci softwarového systému přistupuje prostřednictvím různorodých objektů, a proto vyžadují globální přístupový bod. Samozřejmě, jen když jste si jisti, že nikdy nebudete potřebovat více než jednu instanci, je dobré vsadit, že si to rozmyslíte.

Návrhový vzor Singleton řeší všechny tyto obavy. S designovým vzorem Singleton můžete:

  • Zajistěte, aby byla vytvořena pouze jedna instance třídy
  • Poskytněte globální přístupový bod k objektu
  • V budoucnu povolte více instancí bez ovlivnění klientů třídy singleton

Ačkoli návrhový vzor Singleton - jak dokládá níže uvedený obrázek - je jedním z nejjednodušších návrhových vzorů, představuje pro neopatrného vývojáře Java řadu úskalí. Tento článek pojednává o návrhovém vzoru Singleton a řeší tyto úskalí.

Více o návrhových vzorech Java

Můžete si přečíst vše od Davida Gearyho Sloupce návrhových vzorů Java, nebo zobrazit seznam JavaWorld nejnovější články o návrhových vzorech Java. Viz „Návrhové vzory, velký obraz„pro diskusi o výhodách a nevýhodách používání vzorů Gang of Four. Chcete více? Nechte si doručit bulletin Enterprise Java do své doručené pošty.

Singletonův vzor

v Návrhové vzory: Prvky opakovaně použitelného objektově orientovaného softwaru, Gang of Four popsat Singleton vzor takto:

Zajistěte, aby třída měla pouze jednu instanci, a poskytněte k ní globální přístupový bod.

Na obrázku níže je znázorněn diagram tříd návrhových vzorů Singleton.

Jak vidíte, návrhový vzor Singleton není moc. Singletons udržují statický odkaz na jedinou instanci singletonu a vracejí odkaz na tuto instanci ze statického instance() metoda.

Příklad 1 ukazuje klasickou implementaci návrhového vzoru Singleton:

Příklad 1. Klasický singleton

veřejná třída ClassicSingleton {soukromá statická instance ClassicSingleton = null; protected ClassicSingleton () {// Existuje pouze k poražení instance. } public static ClassicSingleton getInstance () {if (instance == null) {instance = new ClassicSingleton (); } návratová instance; }}

Singleton implementovaný v příkladu 1 je snadno pochopitelný. The ClassicSingleton třída udržuje statický odkaz na instanci osamělého singletonu a vrací tento odkaz ze statické getInstance () metoda.

Existuje několik zajímavých bodů týkajících se ClassicSingleton třída. První, ClassicSingleton využívá techniku ​​známou jako líná instance vytvořit singleton; ve výsledku se instance singleton vytvoří až po getInstance () metoda je volána poprvé. Tato technika zajišťuje, že instance singleton jsou vytvářeny pouze v případě potřeby.

Zadruhé si toho všimněte ClassicSingleton implementuje chráněný konstruktor, takže klienti nemohou vytvářet instance ClassicSingleton instance; Možná vás však překvapí, když zjistíte, že následující kód je naprosto legální:

veřejná třída SingletonInstantiator {public SingletonInstantiator () {ClassicSingleton instance = ClassicSingleton.getInstance (); ClassicSingleton anotherInstance =nový ClassicSingleton (); ... } }

Jak může třída v předchozím fragmentu kódu - který se nerozšiřuje ClassicSingleton-vytvořit ClassicSingleton instance, pokud ClassicSingleton konstruktor je chráněn? Odpověď je, že chráněné konstruktory lze volat podtřídami a jinými třídami ve stejném balíčku. Protože ClassicSingleton a Singletonův vynálezce jsou ve stejném balíčku (výchozí balíček), Singleton Instantiator () metody mohou vytvořit ClassicSingleton instance. Toto dilema má dvě řešení: Můžete vytvořit ClassicSingleton konstruktor soukromý, takže pouze ClassicSingleton () metody tomu říkají; to však znamená ClassicSingleton nelze podtřídu. Někdy je to žádoucí řešení; pokud ano, je dobré prohlásit svoji třídu singletonů finále, což činí tento záměr explicitní a umožňuje kompilátoru použít optimalizace výkonu. Druhým řešením je umístit třídu singleton do explicitního balíčku, takže třídy v jiných balíčcích (včetně výchozího balíčku) nemohou vytvořit instanci singletonových instancí.

Třetí zajímavý bod o ClassicSingleton: Je možné mít více instancí singletonu, pokud třídy načtené různými třídními nakladači přistupují k singletonu. Tento scénář není tak přitažlivý; například některé kontejnery servletů používají pro každý servlet odlišné třídicí programy, takže pokud dva servlety přistupují k singletonu, budou mít každý svou vlastní instanci.

Začtvrté, pokud ClassicSingleton realizuje java.io. Serializovatelné rozhraní, instance třídy lze serializovat a deserializovat. Pokud však serializujete singletonový objekt a následně tento objekt deserializujete více než jednou, budete mít více instancí singleton.

Nakonec a možná nejdůležitější je příklad 1 ClassicSingleton třída není bezpečná pro vlákna. Pokud volají dvě vlákna - budeme jim říkat vlákno 1 a vlákno 2 - ClassicSingleton.getInstance () zároveň dva ClassicSingleton instance lze vytvořit, pokud je vlákno 1 preempted těsně po vstupu do -li blok a řízení je následně dáno vláknu 2.

Jak můžete vidět z předchozí diskuse, ačkoli vzor Singleton je jedním z nejjednodušších návrhových vzorů, jeho implementace v Javě je něco jiného než jednoduchého. Zbytek tohoto článku se věnuje specifickým aspektům prostředí Java pro vzor Singleton, ale nejprve se pojďme krátkou objížďkou podívat, jak si můžeme otestovat své třídy singletonů.

Vyzkoušejte singletony

Ve zbytku tohoto článku používám JUnit společně s log4j k testování tříd singletonů. Pokud nejste obeznámeni s JUnit nebo log4j, podívejte se na Zdroje.

Příklad 2 uvádí testovací případ JUnit, který testuje singleton příkladu 1:

Příklad 2. Jeden testovací případ

import org.apache.log4j.Logger; importovat junit.framework.Assert; import junit.framework.TestCase; veřejná třída SingletonTest rozšiřuje TestCase {soukromý ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger (); public SingletonTest (název řetězce) {super (jméno); } public void setUp () {logger.info ("získávám singleton ..."); sone = ClassicSingleton.getInstance (); logger.info ("... dostal singleton:" + sone); logger.info ("získávám singleton ..."); stwo = ClassicSingleton.getInstance (); logger.info ("... dostal singleton:" + stwo); } public void testUnique () {logger.info ("kontrola rovnosti singletonů"); Assert.assertEquals (true, sone == stwo); }}

Vyvolá se testovací případ z příkladu 2 ClassicSingleton.getInstance () dvakrát a ukládá vrácené odkazy do členských proměnných. The testUnique () metoda zkontroluje, zda jsou odkazy identické. Příklad 3 ukazuje, že výstup testovacího případu:

Příklad 3. Výstup testovacího případu

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) kompilace: run-test-text: [java] .INFO main: dostat singleton... [java] INFO hlavní: vytvořil singleton: Singleton @ e86f41 [java] INFO hlavní: ... dostal singleton: Singleton @ e86f41 [java] INFO hlavní: dostat singleton... [java] INFO hlavní: ... dostal singleton: Singleton @ e86f41 [java] INFO hlavní: kontrola rovnosti singletonů [java] Čas: 0,032 [java] OK (1 test)

Jak ilustruje předchozí seznam, jednoduchý test z příkladu 2 projde letícími barvami - dvěma singletonovými referencemi získanými pomocí ClassicSingleton.getInstance () jsou skutečně identické; tyto odkazy však byly získány v jediném vlákně. Další část zatěžuje naše třídu singletonů s více vlákny.

Aspekty multithreadingu

Příklad 1 ClassicSingleton.getInstance () metoda není bezpečná pro vlákna z důvodu následujícího kódu:

1: if (instance == null) {2: instance = new Singleton (); 3:}

Pokud je vlákno preempted na řádku 2 před provedením přiřazení, instance proměnná člena bude stále nulaa další vlákno může následně vstoupit do souboru -li blok. V takovém případě budou vytvořeny dvě odlišné instance singletonu. K tomuto scénáři bohužel dochází zřídka, a je proto obtížné jej během testování vytvořit. Pro ilustraci ruské rulety s tímto vláknem jsem tento problém vynutil reimplementací třídy příkladu 1. Příklad 4 ukazuje revidovanou třídu singletonů:

Příklad 4. Skládejte balíček

import org.apache.log4j.Logger; public class Singleton {private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger (); soukromý statický boolean firstThread = true; Protected Singleton () {// Existuje pouze k překonání instance. } public static Singleton getInstance () { if (singleton == null) {simulateRandomActivity (); singleton = nový Singleton (); } logger.info ("vytvořen singleton:" + singleton); návrat singleton; } soukromá statická neplatnost simulateRandomActivity() { Snaž se { if (firstThread) {firstThread = false; logger.info ("spící ..."); // Toto zdřímnutí by mělo dát druhé niti dostatek času // dostat se k prvnímu vláknu.Thread.currentThread (). Sleep (50); }} catch (InterruptedException ex) {logger.warn ("Spánek přerušen"); }}}

Singleton z příkladu 4 se podobá třídě z příkladu 1, s výjimkou singletonu v předchozím výpisu, který hromadí balíček, aby vynutil chybu multithreading. Poprvé getInstance () metoda se nazývá, vlákno, které vyvolalo metodu, spí po dobu 50 milisekund, což dává dalšímu vláknu čas na volání getInstance () a vytvořte novou instanci singletonu. Když se spící vlákno probudí, vytvoří také novou instanci singletonu a máme dvě instance singletonu. Ačkoli je třída příkladu 4 vykonstruovaná, stimuluje situaci v reálném světě, kde je první vlákno, které volá getInstance () dostane preempted.

Příklad 5 testuje singleton příkladu 4:

Příklad 5. Test, který selhal

import org.apache.log4j.Logger; importovat junit.framework.Assert; import junit.framework.TestCase; veřejná třída SingletonTest rozšiřuje TestCase {private static Logger logger = Logger.getRootLogger (); soukromý statický Singleton jedináček = null; public SingletonTest (název řetězce) {super (jméno); } public void setUp () { singleton = null; } public void testUnique () vyvolá InterruptedException {// Obě vlákna volají Singleton.getInstance (). Thread threadOne = new Thread (new SingletonTestRunnable ()), threadTwo = new Thread (new SingletonTestRunnable ()); threadOne.start ();threadTwo.start (); threadOne.join (); threadTwo.join (); } soukromá statická třída SingletonTestRunnable implementuje Runnable {public void run () {// Získat odkaz na singleton. Singleton s = Singleton.getInstance (); // Chraňte členskou proměnnou singleton před // vícevláknovým přístupem. synchronized (SingletonTest.class) {if (singleton == null) // Pokud je místní odkaz null ... singleton = s; // ... nastavit na singleton} // Místní odkaz se musí rovnat jedné a // jediné instanci Singletonu; jinak máme dvě instance // Singleton. Assert.assertEquals (true, s == singleton); } } }

Testovací případ z příkladu 5 vytvoří dvě vlákna, spustí každé z nich a čeká na jejich dokončení. Testovací případ udržuje statický odkaz na instanci singletonu a každé vlákno volá Singleton.getInstance (). Pokud statická členská proměnná nebyla nastavena, první vlákno ji nastaví na singleton získaný voláním getInstance ()a statická členská proměnná se porovná s místní proměnnou pro rovnost.

Co se stane, když se spustí testovací případ: Volá první vlákno getInstance (), vstupuje do -li blokovat a spát. Následně volá také druhé vlákno getInstance () a vytvoří instanci singletonu. Druhé vlákno poté nastaví proměnnou statického člena na instanci, kterou vytvořil. Druhé vlákno kontroluje rovnost statické členské proměnné a místní kopie a test projde. Když se první vlákno probudí, vytvoří také instanci singletonu, ale toto vlákno nenastaví proměnnou statického člena (protože ji již nastavilo druhé vlákno), takže statická proměnná a místní proměnná nejsou synchronizované a test pro rovnost selže. Příklad 6 uvádí výstup testovacího případu z příkladu 5:

$config[zx-auto] not found$config[zx-overlay] not found