Programiranje

Kako krmariti po varljivo preprostem vzorcu Singleton

Vzorec Singleton je varljivo preprost, enakomeren in še posebej za razvijalce Java. V tej klasiki JavaWorld članek David Geary prikazuje, kako razvijalci Java implementirajo singletone, s primeri kode za večnitnost, naložitev razredov in serializacijo z vzorcem Singleton. Na koncu zaključi s pogledom na izvajanje enotnih registrov, da bi med izvajanjem določil posamezne enotočke.

Včasih je primerno imeti natančno en primerek razreda: upravitelji oken, vmesniki tiskanja in datotečni sistemi so prototipični primeri. Običajno do teh vrst predmetov - znanih kot enojni - dostopajo različni predmeti v celotnem programskem sistemu in zato zahtevajo globalno dostopno točko. Seveda, ko ste prepričani, da ne boste nikoli potrebovali več kot enega primerka, je dobro, da si premislite.

Vzorec zasnove Singleton obravnava vse te pomisleke. Z vzorcem vzorca Singleton lahko:

  • Prepričajte se, da je ustvarjen samo en primerek razreda
  • Navedite globalno dostopno točko do predmeta
  • Dovoli več primerkov v prihodnosti, ne da bi to vplivalo na odjemalce posameznega razreda

Čeprav je Singleton oblikovalski vzorec - kot spodaj dokazuje spodnja slika - eden najpreprostejših vzorcev oblikovanja, predstavlja za neopaznega razvijalca Java številne pasti. Ta članek obravnava vzorec oblikovanja Singleton in obravnava te pasti.

Več o vzorcih oblikovanja Java

Lahko preberete vse Davida Gearyja Stolpci Java Design Patternsali si oglejte seznam JavaWorldov najnovejši članki o vzorcih oblikovanja Java. Glej "Oblikovalni vzorci, velika slika"za razpravo o prednostih in slabostih uporabe vzorcev Gang of Four. Želite več? Pridobite glasilo Enterprise Java v svojo mapo Prejeto.

Vzorec Singleton

V Vzorci oblikovanja: elementi predmetno usmerjene programske opreme za večkratno uporabo, Skupina štirih opisuje vzorec Singletona takole:

Zagotovite, da ima razred samo en primerek, in zagotovite globalno točko dostopa do njega.

Spodnja slika prikazuje diagram razreda vzorčnega vzorca Singleton.

Kot lahko vidite, vzorca oblikovanja Singleton ni veliko. Enotoni vzdržujejo statični sklic na edini primerek enojnega in vrnejo sklic na ta primerek iz statičnega primer () metoda.

Primer 1 prikazuje klasično izvedbo vzorca Singleton:

Primer 1. Klasični singleton

javni razred ClassicSingleton {zasebni statični primerek ClassicSingleton = null; protected ClassicSingleton () {// Obstaja samo za premagovanje primerka. } javni statični ClassicSingleton getInstance () {if (primer == null) {primer = nov ClassicSingleton (); } vrnitev primerka; }}

Singleton, ki je implementiran v primeru 1, je lahko razumljiv. The ClassicSingleton razred ohranja statični sklic na osamljeni primerek posameznika in ga vrne iz statičnega getInstance () metoda.

Obstaja več zanimivih točk glede ClassicSingleton razred. Najprej, ClassicSingleton uporablja tehniko, znano kot leni primerek ustvariti singleton; kot rezultat, primerek singleton se ustvari šele, ko getInstance () metoda prvič. Ta tehnika zagotavlja, da se enojni primerki ustvarijo le po potrebi.

Drugič, opazite to ClassicSingleton izvaja zaščiteni konstruktor, tako da odjemalci ne morejo ustvariti primerka ClassicSingleton primeri; vendar boste morda presenečeni, ko boste ugotovili, da je naslednja koda popolnoma zakonita:

javni razred SingletonInstantiator {public SingletonInstantiator () {Primer ClassicSingleton = ClassicSingleton.getInstance (); ClassicSingleton anotherInstance =novi ClassicSingleton (); ... } }

Kako lahko razred v prejšnjem fragmentu kode - ki se ne razširi ClassicSingleton-ustvariti ClassicSingleton primer, če ClassicSingleton konstruktor je zaščiten? Odgovor je, da lahko zaščitene konstruktorje imenujemo podrazredi in po drugih razredih v istem paketu. Ker ClassicSingleton in SingletonInstantiator so v istem paketu (privzeti paket), SingletonInstantiator () metode lahko ustvarijo ClassicSingleton primerov. Ta dilema ima dve rešitvi: ClassicSingleton konstruktor private, tako da samo ClassicSingleton () metode ga kličejo; vendar to pomeni ClassicSingleton ni mogoče podrazvrstiti. Včasih je to zaželena rešitev; v tem primeru je dobro, da prijavite razred posameznika dokončno, ki naredi to namero eksplicitno in prevajalniku omogoča uporabo optimizacij zmogljivosti. Druga rešitev je, da razred singleton prenesete v izrecni paket, zato razredi v drugih paketih (vključno s privzetim paketom) ne morejo ustvariti primerkov enojnega primera.

Tretja zanimiva točka o ClassicSingleton: možno je imeti več primerkov posameznika, če razredi, ki jih nalagajo različni naložitelji razredov, dostopajo do posameznega primera. Ta scenarij ni tako domišljen; na primer nekateri vsebniki strežniških programčkov uporabljajo ločene nalagalnike razredov za vsak strežniški programček, tako da če imata dva strežniška programčka dostop do enega samega, bosta imela vsak svoj primerek.

Četrti, če ClassicSingleton izvaja java.io.Serializable vmesnika lahko primerke razreda serializiramo in deserializiramo. Če pa serijski objekt enotno serizirate in ga nato večkrat enkrat deserializirate, boste imeli več primerov enojne enote.

Končno in morda najpomembnejši primer 1 ClassicSingleton razred ni varen za nit. Če pokličeta dve niti - poimenovali ju bomo nit 1 in nit 2 ClassicSingleton.getInstance () hkrati dve ClassicSingleton primere je mogoče ustvariti, če je Thread 1 izključen takoj po vstopu v če blok in nadzor je nato dodeljen niti 2.

Kot lahko vidite iz prejšnje razprave, čeprav je vzorec Singleton eden najpreprostejših vzorcev oblikovanja, je njegovo izvajanje v Javi vse prej kot preprosto. Preostali del tega članka obravnava posebnosti Java za vzorec Singleton, vendar najprej pojdimo na kratek ovitek, da preverimo, kako lahko preizkusite razrede singleton.

Preizkusite samske

V preostalem delu tega članka uporabljam JUnit skupaj z log4j za preizkušanje posameznih razredov. Če niste seznanjeni z JUnit ali log4j, glejte Viri.

Primer 2 navaja testni primer JUnit, ki preizkuša singleton primera 1:

Primer 2. Enotni testni primer

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; javni razred SingletonTest razširja TestCase {private ClassicSingleton sone = null, stwo = null; zasebni statični logger logger = Logger.getRootLogger (); javni SingletonTest (ime niza) {super (ime); } public void setUp () {logger.info ("pridobivanje singletona ..."); sone = ClassicSingleton.getInstance (); logger.info ("... got singleton:" + sone); logger.info ("pridobivanje singletona ..."); stwo = ClassicSingleton.getInstance (); logger.info ("... dobil singleton:" + stwo); } public void testUnique () {logger.info ("preverjanje enakovrednosti enotonov"); Assert.assertEquals (true, sone == stwo); }}

Preizkus primera 2. primera ClassicSingleton.getInstance () dvakrat in vrnjene sklice shrani v spremenljivke člana. The testUnique () metoda preverja, ali so sklici enaki. Primer 3 kaže, da je rezultat testnega primera:

Primer 3. Izhod testnega primera

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) compile: run-test-text: [java] .INFO main: pridobivanje samskega... [java] INFO glavni: ustvaril singleton: Singleton @ e86f41 [java] INFO main: ... got singleton: Singleton @ e86f41 [java] INFO main: pridobivanje samskega... [java] INFO main: ... got singleton: Singleton @ e86f41 [java] INFO main: preverjanje enakovrednosti singletonov [java] Čas: 0,032 [java] OK (1 test)

Kot je razvidno iz prejšnjega seznama, preprost preizkus primera 2 opravi leteče barve - dve enojni referenci, pridobljeni z ClassicSingleton.getInstance () so resnično enaki; vendar so bile te reference pridobljene v eni niti. Naslednji odsek stresno testira naš singleton razred z več nitmi.

Razmisleki o večnitnostih

Primer 1 ClassicSingleton.getInstance () metoda ni varna pred nitmi zaradi naslednje kode:

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

Če je nit v vrstici 2 pred prevzemom prevzeta, se primer spremenljivka člana bo še vedno nič, in druga nit lahko pozneje vstopi v če blok. V tem primeru bosta ustvarjena dva različna primera posameznika. Na žalost se tak scenarij zgodi le redko in ga je zato težko preizkusiti med testiranjem. Za ponazoritev te niti ruske rulete sem težavo izsilil tako, da sem ponovno uporabil razred 1. primera. Primer 4 prikazuje popravljeni razred enojnih datotek:

Primer 4. Zložite krov

import org.apache.log4j.Logger; javni razred Singleton {zasebni statični Singleton singleton = null; zasebni statični logger logger = Logger.getRootLogger (); zasebna statična logična vrednost firstThread = res; protected Singleton () {// Obstaja samo za premagovanje primerka. } javni statični Singleton getInstance () { if (singleton == null) {simulateRandomActivity (); singleton = nov Singleton (); } logger.info ("ustvarjen singleton:" + singleton); vrnitev singleton; } zasebna statična praznina simulatorRandomActivity() {poskusite { če (firstThread) {firstThread = false; logger.info ("spanje ..."); // Ta dremež naj bi dal drugi niti dovolj časa // priti do prve niti.Thread.currentThread (). Sleep (50); }} catch (InterruptedException ex) {logger.warn ("Spanje prekinjeno"); }}}

Singleton primera 4 je podoben razredu primera 1, razen če singleton v prejšnjem seznamu zloži krov, da prisili napako večnitnosti. Prvič getInstance () Pokliče se metoda, nit, ki je priklicala metodo, spi 50 milisekund, kar daje drugi niti čas, da pokliče getInstance () in ustvarite nov primerek posameznika. Ko se spalna nit prebudi, ustvari tudi nov enojni primerek in imamo dva enojna primerka. Čeprav je razred primera 4 izmišljen, spodbuja resnične razmere, ko je prva nit, ki kliče getInstance () dobi prednost.

Primer 5 preizkusov Primer 4 je singleton:

Primer 5. Test, ki ne uspe

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; javni razred SingletonTest razširja TestCase {private static Logger logger = Logger.getRootLogger (); zasebni statični Singleton singleton = nič; javni SingletonTest (ime niza) {super (ime); } javna void setUp () { singleton = null; } public void testUnique () vrže InterruptedException {// Obe niti pokličeta Singleton.getInstance (). Thread threadOne = new Thread (new SingletonTestRunnable ()), threadTwo = new Thread (new SingletonTestRunnable ()); threadOne.start ();threadTwo.start (); threadOne.join (); threadTwo.join (); } zasebni statični razred SingletonTestRunnable izvaja Runnable {public void run () {// Pridobite sklic na singleton. Singleton s = Singleton.getInstance (); // Zaščita spremenljivke posameznega člana pred večnitnim dostopom. sinhronizirano (SingletonTest.class) {if (singleton == null) // Če je lokalni sklic ničen ... singleton = s; // ... nastavimo na singleton}} // Lokalna referenca mora biti enaka enemu in // samo primerku Singleton; v nasprotnem primeru imamo dva primera // Singleton. Assert.assertEquals (true, s == singleton); } } }

Testni primer primera 5 ustvari dve niti, zažene vsako in počaka, da se končata. Testni primer ohranja statični sklic na primerek posameznika in vsaka nit pokliče Singleton.getInstance (). Če spremenljivka statičnega člana ni nastavljena, jo prva nit nastavi na singleton, pridobljen s klicem na getInstance (), statična spremenljivka člana pa se primerja z lokalno spremenljivko za enakost.

Evo, kaj se zgodi, ko se preizkusni primer zažene: prva nit pokliče getInstance (), vstopi v če blok in spi. Nato pokliče tudi druga nit getInstance () in ustvari enojni primerek. Druga nit nato nastavi spremenljivko statičnega člana na primerek, ki ga je ustvaril. Druga nit preveri enakovrednost statične spremenljivke člana in lokalne kopije in test opravi. Ko se prva nit prebudi, ustvari tudi enojni primerek, vendar ta nit ne nastavi spremenljivke statičnega člana (ker jo je druga nit že nastavila), zato statična spremenljivka in lokalna spremenljivka nista sinhronizirani, test saj enakost ne uspe. Primer 6 navaja izhodne primere primera 5:

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