Programovanie

Ako sa pohybovať v klamne jednoduchom singletonskom vzore

Singletonov vzor je klamne jednoduchý, rovnomerný a hlavne pre vývojárov Java. V tejto klasike JavaWorld článok, David Geary demonštruje, ako vývojári Java implementujú singletony, s príkladmi kódov pre viacvláknové spracovanie, načítače tried a serializáciu pomocou vzoru Singleton. Na záver sa zaoberá implementáciou singletonových registrov s cieľom špecifikovať singletony za behu.

Niekedy je vhodné mať práve jednu inštanciu triedy: prototypy príkladov sú správcovia okien, zaraďovače tlače a systémy súborov. K týmto typom objektov - známym ako singletons - sa zvyčajne v rámci softvérového systému pristupuje prostredníctvom nesúrodých objektov, a preto vyžadujú globálny prístupový bod. Samozrejme, ak ste si istí, že už nikdy nebudete potrebovať viac ako jednu inštanciu, je dobré sa staviť, že to rozmyslíte.

Dizajnový vzor Singleton rieši všetky tieto obavy. S dizajnovým vzorom Singleton môžete:

  • Zaistite, aby bola vytvorená iba jedna inštancia triedy
  • Poskytnite globálny prístupový bod k objektu
  • Povoliť v budúcnosti viac inštancií bez ovplyvnenia klientov triedy singleton

Aj keď je návrhový vzor Singleton - o čom svedčí nasledujúci obrázok - jedným z najjednoduchších návrhových vzorov, predstavuje neopatrného vývojára Java množstvo úskalí. Tento článok pojednáva o dizajnovom vzore Singleton a venuje sa týmto nástrahám.

Viac informácií o návrhových vzoroch Java

Môžete si prečítať všetky David Geary Stĺpce Java Design Patterns, alebo zobraziť zoznam svetov JavaWorld najaktuálnejšie články o návrhových vzoroch Java. Pozri „Dizajnové vzory, veľký obraz„na diskusiu o výhodách a nevýhodách používania vzorov Gang of Four. Chcete viac? Získajte doručený bulletin Enterprise Java do svojej schránky.

Singletonov vzor

V Dizajnové vzory: Prvky opakovane použiteľného objektovo orientovaného softvéru, Gang štyroch popisuje singletonský vzor takto:

Zaistite, aby mala trieda iba jednu inštanciu, a poskytnite k nej globálny prístupový bod.

Na obrázku nižšie je znázornený diagram tried vzorových vzorov Singleton.

Ako vidíte, návrhový vzor Singleton nie je veľa. Singletons udržujú statický odkaz na jedinú inštanciu singletonu a vrátia odkaz na túto inštanciu zo statického inštancia () metóda.

Príklad 1 ukazuje klasickú implementáciu návrhového vzoru Singleton:

Príklad 1. Klasický singleton

verejná trieda ClassicSingleton {súkromná statická inštancia ClassicSingleton = null; protected ClassicSingleton () {// Existuje iba na prekonanie inštancie. } public static ClassicSingleton getInstance () {if (instance == null) {instance = new ClassicSingleton (); } návratová inštancia; }}

Singleton implementovaný v príklade 1 je ľahko pochopiteľný. The ClassicSingleton trieda udržiava statický odkaz na osamelú inštanciu singletonu a vráti tento odkaz zo statického getInstance () metóda.

Existuje niekoľko zaujímavých bodov týkajúcich sa ClassicSingleton trieda. Najprv, ClassicSingleton využíva techniku ​​známu ako lenivá inštancia vytvoriť singleton; vo výsledku sa inštancia singleton vytvorí, až keď getInstance () metóda sa volá prvýkrát. Táto technika zaisťuje, že sa jednotlivé inštancie vytvárajú iba v prípade potreby.

Po druhé, všimnite si to ClassicSingleton implementuje chránený konštruktor, takže klienti nemôžu vytvárať inštancie ClassicSingleton inštancie; možno vás však prekvapí, keď zistíte, že nasledujúci kód je úplne legálny:

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

Ako môže trieda v predchádzajúcom fragmente kódu - ktorý sa nerozšíri ClassicSingleton-Vytvor ClassicSingleton príklad, ak ClassicSingleton konštruktor je chránený? Odpoveďou je, že chránené konštruktory možno nazvať podtriedami a inými triedami v rovnakom balíku. Pretože ClassicSingleton a Singleton vynálezca sú v rovnakom balíku (predvolený balík), Singleton Instantiator () môžu vytvoriť metódy ClassicSingleton inštancie. Táto dilema má dve riešenia: Môžete vytvoriť ClassicSingleton konštruktér súkromný, takže iba ClassicSingleton () metódy to nazývajú; to však znamená ClassicSingleton nemožno zaradiť do podtriedy. Niekedy je to žiaduce riešenie; ak je to tak, je dobré vyhlásiť svoju triedu singletonov konečné, ktorý robí tento zámer explicitným a umožňuje kompilátoru použiť optimalizáciu výkonu. Ďalším riešením je vložiť vašu triedu singleton do explicitného balíka, takže triedy v iných balíkoch (vrátane predvoleného balíka) nemôžu vytvárať inštancie singletonových inštancií.

Tretí zaujímavý bod o ClassicSingleton: je možné mať viac inštancií singletonu, ak triedy načítané rôznymi načítavačmi tried pristupujú k singletonu. Tento scenár nie je taký priťahovaný; napríklad niektoré kontajnery servletov používajú pre každý servlet odlišné triedičky, takže ak dva servlety pristupujú k singletonu, budú mať každý svoju vlastnú inštanciu.

Po štvrté, ak ClassicSingleton realizuje java.io. Serializovateľné rozhranie, inštancie triedy môžu byť serializované a deserializované. Ak však serializujete singletonový objekt a následne tento objekt deserializujete viackrát, budete mať viac singletonových inštancií.

A nakoniec, a možno najdôležitejšie, príklad 1 ClassicSingleton trieda nie je bezpečná pre vlákna. Ak dve vlákna - zavoláme im vlákno 1 a vlákno 2 - zavolajte ClassicSingleton.getInstance () zároveň dva ClassicSingleton inštancie je možné vytvoriť, ak je vlákno 1 predvídané hneď po vstupe do súboru ak blok a riadenie je následne dané vláknu 2.

Ako vidíte z predchádzajúcej diskusie, aj keď je vzor Singleton jedným z najjednoduchších vzorov návrhu, jeho implementácia v Jave je všetko, len nie jednoduchá. Zvyšok tohto článku sa venuje úvahám špecifickým pre Javu pre vzor Singleton, najskôr sa však urobme krátku obchádzku, aby sme zistili, ako si môžete otestovať svoje triedy singleton.

Vyskúšajte singletony

V celom zvyšku tohto článku používam JUnit spolu s log4j na testovanie tried singletonu. Ak nie ste oboznámení s JUnit alebo log4j, pozrite si Zdroje.

Príklad 2 uvádza testovací prípad JUnit, ktorý testuje singleton Príkladu 1:

Príklad 2. Jeden testovací prípad

import org.apache.log4j.Logger; importovať junit.framework.Assert; import junit.framework.TestCase; verejná trieda SingletonTest rozširuje TestCase {private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger (); public SingletonTest (Názov reťazca) {super (meno); } public void setUp () {logger.info ("získavam singleton ..."); sone = ClassicSingleton.getInstance (); logger.info ("... dostal singleton:" + sone); logger.info ("získavam singleton ..."); stwo = ClassicSingleton.getInstance (); logger.info ("... dostal singleton:" + stwo); } public void testUnique () {logger.info ("kontrola rovnosti singletonov"); Assert.assertEquals (true, sone == stwo); }}

Vyvolá sa testovací prípad z príkladu 2 ClassicSingleton.getInstance () dvakrát a vráti vrátené referencie do členských premenných. The testUnique () metóda skontroluje, či sú referencie identické. Príklad 3 ukazuje, že výstup testovacieho prípadu:

Príklad 3. Výstup testovacieho prípadu

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) kompilácia: run-test-text: [java] .INFO main: dostať singleton... [java] INFO hlavné: vytvoril singleton: Singleton @ e86f41 [java] INFO main: ... dostal singleton: Singleton @ e86f41 [java] INFO main: dostať singleton... [java] INFO main: ... dostal singleton: Singleton @ e86f41 [java] INFO main: kontrola rovnosti singletonov [java] Čas: 0,032 [java] OK (1 test)

Ako ilustruje predchádzajúci zoznam, jednoduchý test z príkladu 2 vyhovuje splývavým farbám - dvomi singletonovými odkazmi získanými pomocou ClassicSingleton.getInstance () sú skutočne identické; tieto odkazy však boli získané v jednom vlákne. Ďalšia časť podrobuje záťažovým testom našu triedu singletonov s viacerými vláknami.

Úvahy o viacerých vláknach

Príklady 1 ClassicSingleton.getInstance () metóda nie je bezpečná pre vlákna z dôvodu nasledujúceho kódu:

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

Ak je vlákno na riadku 2 pred vykonaním priradenia predbiehané, inštancia premenná člena bude stále nulovýa ďalšie vlákno môže následne vstúpiť do súboru ak blokovať. V takom prípade sa vytvoria dva odlišné inštancie jednotlivca. Tento scenár sa bohužiaľ vyskytuje zriedka, a je preto ťažké ho počas testovania vytvoriť. Na ilustráciu ruskej rulety s týmto vláknom som tento problém vynútil opätovným zavedením triedy z príkladu 1. Príklad 4 ukazuje revidovanú triedu singleton:

Príklad 4. Skladanie paluby

import org.apache.log4j.Logger; public class Singleton {private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger (); private static boolean firstThread = pravda; Protected Singleton () {// Existuje iba na prekonanie inštancie. } verejný statický Singleton getInstance () { if (singleton == null) {simulateRandomActivity (); singleton = nový Singleton (); } logger.info ("vytvorený singleton:" + singleton); návrat singleton; } private static void simulateRandomActivity() {vyskúšať { if (firstThread) {firstThread = false; logger.info ("spí ..."); // Tento spánok by mal dať druhému vláknu dostatok času // dostať sa k prvému vláknu.Thread.currentThread (). Spánok (50); }} catch (InterruptedException ex) {logger.warn ("Spánok prerušený"); }}}

Singleton z príkladu 4 sa podobá triede z príkladu 1, s výnimkou singletonu v predchádzajúcom zozname, ktorý zoskupuje balíček, aby vynútil chybu viacerých vlákien. Prvýkrát getInstance () metóda sa volá, vlákno, ktoré vyvolalo túto metódu, spí 50 milisekúnd, čo dáva ďalšiemu vláknu čas na volanie getInstance () a vytvorte novú inštanciu singleton. Keď sa spánkové vlákno prebudí, vytvorí sa tiež nová inštancia singletonu a máme dve inštancie singletonu. Aj keď je trieda príkladu 4 vykonštruovaná, stimuluje situáciu v reálnom svete, keď ide o prvé vlákno, ktoré volá getInstance () dostane prednosť.

Príklad 5 testuje singleton príkladu 4:

Príklad 5. Test, ktorý zlyhá

import org.apache.log4j.Logger; importovať junit.framework.Assert; import junit.framework.TestCase; verejná trieda SingletonTest rozširuje TestCase {private static Logger logger = Logger.getRootLogger (); súkromný statický Singleton singleton = null; public SingletonTest (Názov reťazca) {super (meno); } public void setUp () { singleton = null; } public void testUnique () vyvolá InterruptedException {// Obidve vlákna volajú 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 () {// Získať odkaz na singleton. Singleton s = Singleton.getInstance (); // Chrániť jednotlivú členskú premennú pred // viacvláknovým prístupom. synchronized (SingletonTest.class) {if (singleton == null) // Ak je miestny odkaz null ... singleton = s; // ... nastavte ho na singleton} // Lokálny odkaz sa musí rovnať jednej a // jedinej inštancii Singleton; inak máme dve inštancie // Singleton. Assert.assertEquals (true, s == singleton); } } }

Testovací prípad z príkladu 5 vytvorí dve vlákna, každé z nich spustí a čaká na ich dokončenie. Testovací prípad zachováva statický odkaz na jednu inštanciu a každé vlákno volá Singleton.getInstance (). Ak statická členská premenná nebola nastavená, prvé vlákno ju nastaví na singleton získaný volaním getInstance ()a premenná statického člena sa porovnáva s lokálnou premennou kvôli rovnosti.

Pri spustení testovacieho prípadu sa stane toto: Zavolá sa prvé vlákno getInstance (), vstupuje do ak blokovať, a spí. Následne zavolá aj druhé vlákno getInstance () a vytvorí jednotlivú inštanciu. Druhé vlákno potom nastaví premennú statického člena na inštanciu, ktorú vytvorila. Druhé vlákno skontroluje, či sú statické členské premenné a lokálna kópia rovnocenné a test vyhovuje. Keď sa prvé vlákno prebudí, vytvorí sa tiež singletonová inštancia, ale toto vlákno nenastaví premennú statického člena (pretože ju už nastavilo druhé vlákno), takže statická premenná a lokálna premenná nie sú synchronizované a test pre rovnosť zlyháva. Príklad 6 uvádza výstup testovacieho prípadu z príkladu 5:

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