Ohjelmointi

Kuinka navigoida petollisen yksinkertaisessa Singleton-kuviossa

Singleton-malli on petollisen yksinkertainen, tasainen ja erityisesti Java-kehittäjille. Tässä klassikossa JavaWorld artikkeli, David Geary osoittaa, kuinka Java-kehittäjät toteuttavat singletoneja koodiesimerkkien avulla monisäikeisiin, luokkakuormaajiin ja sarjallisuuteen Singleton-mallin avulla. Hän päättää tarkastelemalla yksirekisterirekisterien toteuttamista singletonien määrittämiseksi ajon aikana.

Joskus on tarkoituksenmukaista, että luokassa on täsmälleen yksi esiintymä: ikkunaohjaimet, tulostus taustatulostimet ja tiedostojärjestelmät ovat prototyyppisiä esimerkkejä. Tyypillisesti tämän tyyppisiin kohteisiin, joita kutsutaan yksinäisiksi, pääsee käsiksi erilaisilla esineillä kaikkialla ohjelmistojärjestelmässä, ja siksi ne edellyttävät globaalia pääsypistettä. Tietenkin juuri silloin, kun olet varma, et koskaan tarvitse enempää kuin yhtä esiintymää, se on hyvä veto, että muutat mieltäsi.

Singletonin suunnittelukuvio käsittelee kaikkia näitä huolenaiheita. Singleton-mallikuvion avulla voit:

  • Varmista, että luokkaan luodaan vain yksi esiintymä
  • Anna objektille maailmanlaajuinen pääsypiste
  • Salli useita esiintymiä tulevaisuudessa vaikuttamatta yksittäisen luokan asiakkaisiin

Vaikka Singletonin suunnittelumalli - kuten alla olevasta kuvasta käy ilmi - on yksi yksinkertaisimmista suunnittelumalleista, se tuo esiin useita karhuja varomattomalle Java-kehittäjälle. Tässä artikkelissa käsitellään Singletonin suunnittelumallia ja puututaan näihin sudenkuoppiin.

Lisätietoja Java-suunnittelumalleista

Voit lukea kaikki David Gearyn Java Design Patterns -sarakkeettai tarkastele JavaWorld-luetteloa viimeisimmät artikkelit Java-suunnittelumalleista. Katso "Suunnittelumallit, iso kuva"keskusteluun Neljä joukko -mallien käytön eduista ja haitoista. Haluatko lisää? Hanki Enterprise Java -uutiskirje postilaatikkoosi.

Singleton-kuvio

Sisään Suunnittelumallit: Uudelleenkäytettävien olio-ohjelmistojen elementit, Neljän jengi kuvaa Singleton-mallia näin:

Varmista, että luokassa on vain yksi esiintymä, ja anna sille yleinen pääsypiste.

Alla oleva kuva havainnollistaa Singletonin suunnittelukuvion luokkakaaviota.

Kuten näette, Singletonin suunnittelumallissa ei ole paljon. Yksittäiset henkilöt ylläpitävät staattista viittausta ainoaan yksittäiseen esiintymään ja palauttavat viitteen kyseiseen esiintymään staattisesta ilmentymä() menetelmä.

Esimerkki 1 esittää klassisen Singleton-mallin toteutuksen:

Esimerkki 1. Klassinen singletti

julkinen luokka ClassicSingleton {yksityinen staattinen ClassicSingleton-ilmentymä = null; suojattu ClassicSingleton () {// Olemassa vain estämään instantiation. } public staattinen ClassicSingleton getInstance () {if (instance == null) {instance = new ClassicSingleton (); } return-ilmentymä; }}

Esimerkissä 1 toteutettu yksikkö on helppo ymmärtää. ClassicSingleton luokka ylläpitää staattista viittausta yksinäiseen yksittäistapaukseen ja palauttaa viitteen staattisesta getInstance () menetelmä.

On useita mielenkiintoisia kohtia liittyen ClassicSingleton luokassa. Ensimmäinen, ClassicSingleton käyttää tekniikkaa, joka tunnetaan nimellä laiska instantiation luoda singleton; tämän seurauksena singleton-ilmentymä luodaan vasta getInstance () menetelmää kutsutaan ensimmäistä kertaa. Tämä tekniikka varmistaa, että yksittäisiä esiintymiä luodaan vain tarvittaessa.

Toiseksi, huomaa se ClassicSingleton toteuttaa suojatun rakentajan, joten asiakkaat eivät voi ilmetä ClassicSingleton tapaukset; Saatat kuitenkin olla yllättynyt huomatessasi, että seuraava koodi on täysin laillinen:

julkinen luokka SingletonInstantiator {public SingletonInstantiator () {ClassicSingleton instance = ClassicSingleton.getInstance (); ClassicSingleton anotherInstance =uusi ClassicSingleton (); ... } }

Kuinka edellisen koodin luokka voi katkaista - joka ei ulotu ClassicSingleton-luo ClassicSingleton esimerkiksi jos ClassicSingleton rakentaja on suojattu? Vastaus on, että suojattuja rakentajia voidaan kutsua alaluokkiin ja muiden luokkien samassa paketissa. Koska ClassicSingleton ja Yksittäinen välittäjä ovat samassa paketissa (oletuspaketti), Yksittäinen välittäjä () menetelmät voivat luoda ClassicSingleton tapauksia. Tässä dilemmassa on kaksi ratkaisua: Voit tehdä ClassicSingleton rakentaja yksityinen niin vain KlassinenSingleton () menetelmät kutsuvat sitä; se tarkoittaa kuitenkin ClassicSingleton ei voida luokitella alaluokkiin. Joskus se on toivottava ratkaisu; jos on, on hyvä julistaa yksinäisyystunnisi lopullinen, mikä tekee tarkoituksesta selkeän ja antaa kääntäjän soveltaa suorituskyvyn optimointeja. Toinen ratkaisu on laittaa singleton-luokkasi eksplisiittiseen pakettiin, joten muiden pakettien luokat (mukaan lukien oletuspaketti) eivät voi ilmentää singleton-ilmentymiä.

Kolmas mielenkiintoinen asia ClassicSingleton: On mahdollista saada useita yksittäisiä esiintymiä, jos eri luokkakuormaajien lataamat luokat käyttävät yksikköä. Tuo skenaario ei ole niin haettu; esimerkiksi jotkut servlet-kontit käyttävät erillisiä luokkakuormaajia kullekin servletille, joten jos kaksi servletiä käyttää singlettiä, niillä on jokaisella oma ilmentymä.

Neljänneksi, jos ClassicSingleton toteuttaa java.io.Serialisoitavissa käyttöliittymän avulla luokan instanssit voidaan sarjoittaa ja deserialisoida. Jos kuitenkin sarjalliset yksittäisen objektin ja deserialisoit sen myöhemmin useammin kuin kerran, sinulla on useita yksittäisiä esiintymiä.

Lopuksi ja ehkä tärkein esimerkki 1 ClassicSingleton luokka ei ole langankestävä. Jos kaksi ketjua - kutsumme heitä säie 1 ja lanka 2 - kutsuu ClassicSingleton.getInstance () samaan aikaan kaksi ClassicSingleton ilmentymiä voidaan luoda, jos lanka 1 on ennalta asetettu heti sen saapumisen jälkeen jos lohko ja ohjaus annetaan sen jälkeen langalle 2.

Kuten edellisestä keskustelusta käy ilmi, vaikka Singleton-malli on yksi yksinkertaisimmista suunnittelumalleista, sen toteuttaminen Java-ohjelmassa on kaikkea muuta kuin yksinkertaista. Tämän artikkelin loppuosa käsittelee Java-spesifisiä näkökohtia Singleton-mallille, mutta ensin otetaan lyhyt kiertotie nähdäksesi, kuinka voit testata singleton-luokkiasi.

Testaa yksittäisiä

Tämän artikkelin loppuosassa käytän JUnitia yhdessä log4j: n kanssa testattaakseni yksittäisiä luokkia. Jos et tunne JUnitia tai log4j: tä, katso Resurssit.

Esimerkissä 2 luetellaan JUnit-testitapaus, joka testaa esimerkin 1 singletonin:

Esimerkki 2. Yksittäistesti

tuo org.apache.log4j.Logger; tuo junit.framework.Assert; tuo junit.framework.TestCase; julkinen luokka SingletonTest laajentaa TestCase {yksityinen ClassicSingleton sone = null, stwo = null; yksityinen staattinen Logger logger = Logger.getRootLogger (); julkinen SingletonTest (merkkijono) {super (nimi); } public void setUp () {logger.info ("singletonin hakeminen ..."); sone = ClassicSingleton.getInstance (); logger.info ("... sai singleton:" + sone); logger.info ("singletonin saaminen ..."); stwo = ClassicSingleton.getInstance (); logger.info ("... sai singleton:" + stwo); } public void testUnique () {logger.info ("singletonien tasa-arvon tarkistaminen"); Assert.assertEquals (true, sone == stwo); }}

Esimerkin 2 testitapaus vetoaa ClassicSingleton.getInstance () kahdesti ja tallentaa palautetut viitteet jäsenmuuttujiin. testUnique () menetelmä tarkistaa, että viitteet ovat identtiset. Esimerkki 3 osoittaa, että testitulos:

Esimerkki 3. Testitapaustulos

Koontitiedosto: build.xml init: [echo] Koontiversio 20030414 (14.4.2003 03:08) compile: run-test-text: [java] .INFO main: saada singleton... [java] INFO pää: luonut singletonin: Singleton @ e86f41 [java] INFO main: ... sai singleton: Singleton @ e86f41 [java] INFO main: saada singleton.

Kuten edellinen luettelo havainnollistaa, esimerkin 2 yksinkertainen testi läpäisee lentävät värit - kaksi yksittäistä viittausta, jotka on saatu ClassicSingleton.getInstance () ovat todellakin samanlaisia; nämä viittaukset saatiin kuitenkin yhdellä säikeellä. Seuraava osa testaa yksinkertaisuusluokkamme useilla säikeillä.

Monisäikeiset näkökohdat

Esimerkit 1 ClassicSingleton.getInstance () menetelmä ei ole langankestävä seuraavan koodin takia:

1: if (instanssi == null) {2: instanssi = uusi Singleton (); 3:}

Jos ketju on ennalta asetettu rivillä 2 ennen tehtävän tekemistä, ilmentymä jäsenmuuttuja on edelleen tyhjä, ja toinen ketju voi myöhemmin syöttää jos lohko. Tällöin luodaan kaksi erillistä yksittäistä esiintymää. Valitettavasti tätä skenaariota esiintyy harvoin ja sitä on siksi vaikea tuottaa testauksen aikana. Tämän venäläisen ruletin havainnollistamiseksi olen pakottanut ongelman toteuttamalla esimerkin 1 luokan uudelleen. Esimerkki 4 esittää tarkistetun singleton-luokan:

Esimerkki 4. Pinoa kansi

tuo org.apache.log4j.Logger; julkinen luokka Singleton {yksityinen staattinen Singleton singleton = null; yksityinen staattinen Logger logger = Logger.getRootLogger (); yksityinen staattinen looginen firstThread = tosi; suojattu Singleton () {// Olemassa vain päihittämisen estämiseksi. } julkinen staattinen Singleton getInstance () { if (singleton == null) {simuloiRandomActivity (); singleton = uusi Singleton (); } logger.info ("luotu singleton:" + singleton); paluu singleton; } yksityinen staattinen mitätöinti simuloiRandomActivity() { yrittää { if (firstThread) {firstThread = väärä; logger.info ("nukkuva ..."); // Tämän torkutuksen pitäisi antaa toiselle langalle tarpeeksi aikaa // päästä ensimmäisestä langasta.Thread.currentThread (). Lepotila (50); }} catch (InterruptedException ex) {logger.warn ("Lepotila keskeytetty"); }}}

Esimerkin 4 singletti muistuttaa esimerkin 1 luokkaa, paitsi että edellisen listan singletti pinottaa kannen pakottaakseen monisäikeisvirheen. Ensimmäistä kertaa getInstance () method kutsutaan, menetelmän kutsunut säie nukkuu 50 millisekuntia, mikä antaa toiselle säikeelle aikaa kutsua getInstance () ja luo uusi yksittäinen esiintymä. Kun unilanka herää, se luo myös uuden singleton-ilmentymän, ja meillä on kaksi singleton-esiintymää. Vaikka esimerkin 4 luokka on keksitty, se stimuloi todellista tilannetta, jossa ensimmäinen lanka, joka kutsuu getInstance () saa ennalta.

Esimerkki 5 testaa esimerkin 4 singletonin:

Esimerkki 5. Testi, joka epäonnistuu

tuo org.apache.log4j.Logger; tuo junit.framework.Assert; tuo junit.framework.TestCase; julkinen luokka SingletonTest laajentaa TestCase {private static Logger logger = Logger.getRootLogger (); yksityinen staattinen Singleton singleton = nolla; julkinen SingletonTest (merkkijono) {super (nimi); } public void setUp () { singleton = nolla; } public void testUnique () heittää InterruptedException {// Molemmat säikeet kutsuvat Singleton.getInstance (). Säie threadOne = uusi säie (uusi SingletonTestRunnable ()), threadTwo = uusi säie (uusi SingletonTestRunnable ()); threadOne.start ();threadTwo.start (); threadOne.join (); threadTwo.join (); } yksityinen staattinen luokka SingletonTestRunnable toteuttaa Runnable {public void run () {// Hae viittaus singletoniin. Singleton s = Singleton.getInstance (); // Suojaa yksittäinen jäsenmuuttuja // monisäikeisestä pääsystä. synkronoitu (SingletonTest.class) {if (singleton == null) // Jos paikallinen viittaus on tyhjä ... singleton = s; // ... aseta se yksinkertaiseksi} // Paikallisen viitteen on oltava yhtä suuri kuin Singletonin yksi ja // ainoa esiintymä; muuten meillä on kaksi // Singleton-esiintymää. Assert.assertEquals (tosi, s == yksittäinen); } } }

Esimerkin 5 testitapaus luo kaksi ketjua, aloittaa kukin ja odottaa niiden päättymistä. Testitapaus ylläpitää staattista viittausta yksittäistapaukseen, ja jokainen ketju kutsuu Singleton.getInstance (). Jos staattista jäsenmuuttujaa ei ole asetettu, ensimmäinen säie asettaa sen yksikköön, joka on saatu kutsun kanssa getInstance (), ja staattista jäsenmuuttujaa verrataan tasa-arvon paikalliseen muuttujaan.

Näin tapahtuu, kun testitapaus suoritetaan: Ensimmäinen ketju kutsuu getInstance (), tulee jos estää ja nukkuu. Myöhemmin toinen ketju myös kutsuu getInstance () ja luo yksittäisen esiintymän. Toinen säie asettaa sitten staattisen jäsenmuuttujan luomalle ilmentymälle. Toinen säie tarkistaa staattisen jäsenmuuttujan ja paikallisen kopion tasa-arvon ja testi läpäisee. Kun ensimmäinen ketju herää, se luo myös yksittäisen esiintymän, mutta kyseinen säie ei aseta staattista jäsenmuuttujaa (koska toinen säie on jo asettanut sen), joten staattinen muuttuja ja paikallinen muuttuja eivät ole synkronoituja, ja testi sillä tasa-arvo epäonnistuu. Esimerkissä 6 luetellaan esimerkin 5 testitapaustulos: