Programmering

Hvordan navigere i det villedende enkle Singleton-mønsteret

Singleton-mønsteret er villedende enkelt, jevnt og spesielt for Java-utviklere. I denne klassikeren JavaWorld artikkel demonstrerer David Geary hvordan Java-utviklere implementerer singletons, med kodeeksempler for multithreading, classloaders og serialisering ved hjelp av Singleton-mønsteret. Han avslutter med å se på implementering av singletonregistre for å spesifisere singletons ved kjøretid.

Noen ganger er det hensiktsmessig å ha nøyaktig én forekomst av en klasse: vindusadministratorer, utskriftskø og filsystemer er prototypiske eksempler. Vanligvis er disse objektene - kjent som singletoner - tilgjengelige med forskjellige objekter i et programvaresystem, og krever derfor et globalt tilgangspunkt. Akkurat når du er sikker på at du aldri trenger mer enn én forekomst, er det en god innsats at du ombestemmer deg.

Singleton-designmønsteret adresserer alle disse bekymringene. Med Singleton designmønsteret kan du:

  • Forsikre deg om at bare en forekomst av en klasse opprettes
  • Gi et globalt tilgangspunkt til objektet
  • Tillat flere forekomster i fremtiden uten å påvirke klientene til en singleton-klasse

Selv om Singleton-designmønsteret - som det fremgår av figuren nedenfor - er et av de enkleste designmønstrene, presenterer det en rekke fallgruver for den uforsiktige Java-utvikleren. Denne artikkelen diskuterer Singleton designmønsteret og adresserer disse fallgruvene.

Mer om Java designmønstre

Du kan lese hele David Gearys Java Design Patterns-kolonner, eller se en liste over JavaWorlds de siste artiklene om Java designmønstre. Se "Designmønstre, det store bildet"for en diskusjon om fordeler og ulemper ved å bruke mønstrene Gang of Four. Vil du ha mer? Få Enterprise Java-nyhetsbrevet levert til innboksen din.

Singleton-mønsteret

I Designmønstre: Elementer av gjenbrukbar objektorientert programvare, beskriver Gang of Four Singleton-mønsteret slik:

Sørg for at en klasse bare har en forekomst, og gi et globalt tilgangspunkt til den.

Figuren nedenfor illustrerer Singleton designmønster klassediagram.

Som du kan se, er det ikke mye i Singleton-designmønsteret. Singletons opprettholder en statisk referanse til den eneste singletoninstansen og returnerer en referanse til den instansen fra en statisk forekomst() metode.

Eksempel 1 viser en klassisk implementering av Singleton-designmønster:

Eksempel 1. Den klassiske singleton

offentlig klasse ClassicSingleton {privat statisk ClassicSingleton-forekomst = null; beskyttet ClassicSingleton () {// eksisterer bare for å bekjempe instantiering. } offentlig statisk ClassicSingleton getInstance () {if (forekomst == null) {forekomst = ny ClassicSingleton (); } returnere forekomst; }}

Singleton implementert i eksempel 1 er lett å forstå. De ClassicSingleton klasse opprettholder en statisk referanse til den eneste singleton-forekomsten og returnerer referansen fra den statiske getInstance () metode.

Det er flere interessante punkter angående ClassicSingleton klasse. Først, ClassicSingleton benytter en teknikk kjent som lat instantiering å lage singleton; som et resultat blir singleton-forekomsten ikke opprettet før getInstance () metoden kalles for første gang. Denne teknikken sørger for at singleton-forekomster bare opprettes når det er nødvendig.

For det andre, legg merke til det ClassicSingleton implementerer en beskyttet konstruktør slik at klienter ikke kan starte ClassicSingleton tilfeller; imidlertid kan du bli overrasket over å oppdage at følgende kode er helt lovlig:

offentlig klasse SingletonInstantiator {offentlig SingletonInstantiator () {ClassicSingleton-forekomst = ClassicSingleton.getInstance (); ClassicSingleton anotherInstance =nye ClassicSingleton (); ... } }

Hvordan kan klassen i det foregående kodefragmentet - som ikke strekker seg ClassicSingleton-lage en ClassicSingleton eksempel hvis ClassicSingleton konstruktøren er beskyttet? Svaret er at beskyttede konstruktører kan kalles av underklasser og av andre klasser i samme pakke. Fordi ClassicSingleton og SingletonInstantiator er i samme pakke (standardpakken), SingletonInstantiator () metoder kan skape ClassicSingleton tilfeller. Dette dilemmaet har to løsninger: Du kan lage ClassicSingleton konstruktør privat slik at bare ClassicSingleton () metoder kaller det; det betyr imidlertid ClassicSingleton kan ikke underklasseres. Noen ganger er det en ønskelig løsning; I så fall er det en god ide å erklære singleton-klassen endelig, som gjør denne intensjonen eksplisitt og lar kompilatoren bruke ytelsesoptimaliseringer. Den andre løsningen er å sette singleton-klassen din i en eksplisitt pakke, slik at klasser i andre pakker (inkludert standardpakken) ikke kan instantiere singleton-forekomster.

Et tredje interessant poeng om ClassicSingleton: det er mulig å ha flere singleton-forekomster hvis klasser lastet av forskjellige classloaders får tilgang til en singleton. Det scenariet er ikke så langt hentet; for eksempel bruker noen servletcontainere forskjellige klasselastere for hver servlet, så hvis to servlets får tilgang til en singleton, vil de hver ha sin egen forekomst.

Fjerde, hvis ClassicSingleton implementerer java.io Serialiserbar grensesnitt, kan klassens forekomster serieiseres og deserialiseres. Imidlertid, hvis du serialiserer et singleton-objekt og deretter deserialiserer det objektet mer enn en gang, vil du ha flere singleton-forekomster.

Til slutt, og kanskje det viktigste, eksempel 1 ClassicSingleton klasse er ikke trådsikker. Hvis to tråder - vi kaller dem tråd 1 og tråd 2 - ringer ClassicSingleton.getInstance () samtidig to ClassicSingleton tilfeller kan opprettes hvis tråd 1 er forhåndsbestemt like etter at den kommer inn i hvis blokkering og kontroll blir deretter gitt til tråd 2.

Som du kan se fra forrige diskusjon, er Singleton-mønsteret et av de enkleste designmønstrene, men implementering av det i Java er alt annet enn enkelt. Resten av denne artikkelen tar for seg Java-spesifikke betraktninger for Singleton-mønsteret, men la oss først ta en kort avstikker for å se hvordan du kan teste singleton-klassene dine.

Test singletoner

Gjennom resten av denne artikkelen bruker jeg JUnit i konsert med log4j for å teste singleton-klasser. Hvis du ikke er kjent med JUnit eller log4j, se Ressurser.

Eksempel 2 viser en JUnit-testtilfelle som tester eksemplet på singleton:

Eksempel 2. En singleton testtilfelle

importer org.apache.log4j.Logger; importere junit.framework.Assert; importere junit.framework.TestCase; offentlig klasse SingletonTest utvider TestCase {private ClassicSingleton sone = null, stwo = null; privat statisk loggerlogger = Logger.getRootLogger (); offentlig SingletonTest (strengnavn) {super (navn); } public void setUp () {logger.info ("getting singleton ..."); sone = ClassicSingleton.getInstance (); logger.info ("... fikk singleton:" + sone); logger.info ("får singleton ..."); stwo = ClassicSingleton.getInstance (); logger.info ("... fikk singleton:" + stwo); } public void testUnique () {logger.info ("kontrollerer singletons for likhet"); Assert.assertEquals (true, sone == stwo); }}

Eksempel 2s prøvesak påkaller ClassicSingleton.getInstance () to ganger og lagrer de returnerte referansene i medlemsvariabler. De testUnique () metode sjekker for å se at referansene er identiske. Eksempel 3 viser at test case output:

Eksempel 3. Test case output

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) kompilering: run-test-text: [java] .INFO main: får singleton... [java] INFO hoved: opprettet singleton: Singleton @ e86f41 [java] INFO main: ... fikk singleton: Singleton @ e86f41 [java] INFO main: får singleton... [java] INFO main: ... fikk singleton: Singleton @ e86f41 [java] INFO main: sjekker singletons for likhet [java] Tid: 0,032 [java] OK (1 test)

Som den forrige oppføringen illustrerer, består eksempel 2s enkle test med glans - de to singleton-referansene oppnådd med ClassicSingleton.getInstance () er faktisk identiske; disse referansene ble imidlertid innhentet i en enkelt tråd. Den neste delen stresstester vår singleton-klasse med flere tråder.

Flertrådingshensyn

Eksempel 1 ClassicSingleton.getInstance () metoden er ikke trådsikker på grunn av følgende kode:

1: hvis (forekomst == null) {2: forekomst = ny Singleton (); 3:}

Hvis en tråd er forhåndsbestemt på linje 2 før oppdraget blir gjort, forekomst medlemsvariabelen vil fortsatt være null, og en annen tråd kan deretter komme inn i hvis blokkere. I så fall vil to forskjellige singleton-forekomster bli opprettet. Dessverre forekommer det scenario sjelden og er derfor vanskelig å produsere under testing. For å illustrere denne tråden russisk rulett, har jeg tvunget problemet ved å implementere klasse 1 på eksempel 1 på nytt. Eksempel 4 viser den reviderte singleton-klassen:

Eksempel 4. Stakk dekket

importer org.apache.log4j.Logger; offentlig klasse Singleton {privat statisk Singleton singleton = null; privat statisk loggerlogger = Logger.getRootLogger (); privat statisk boolsk førstTråd = sant; beskyttet Singleton () {// Eksisterer bare for å bekjempe instantiering. } offentlig statisk Singleton getInstance () { hvis (singleton == null) {simulateRandomActivity (); singleton = nye Singleton (); } logger.info ("opprettet singleton:" + singleton); retur singleton; } privat statisk tomrom simulateRandomActivity() {prøv { if (firstThread) {firstThread = false; logger.info ("sover ..."); // Denne luren skal gi den andre tråden nok tid // å komme etter den første tråden.Tråd.strømTråd (). Søvn (50); }} fange (InterruptedException ex) {logger.warn ("Søvn avbrutt"); }}}

Eksempel 4s singleton ligner på klasse 1 i Eksempel 1, bortsett fra at singleton i forrige liste stabler kortstokken for å tvinge en flertrådingsfeil. Første gang getInstance () metoden kalles, tråden som påkalte metoden sover i 50 millisekunder, noe som gir en annen tråd tid til å ringe getInstance () og opprett en ny singleton-forekomst. Når sovetråden våkner, skaper den også en ny singleton-forekomst, og vi har to singleton-forekomster. Selv om klasse 4 i eksempel 4 er konstruert, stimulerer den den virkelige situasjonen der den første tråden som ringer getInstance () blir forhåndsbestemt.

Eksempel 5 tester Eksempel 4s singleton:

Eksempel 5. En test som mislykkes

importer org.apache.log4j.Logger; importere junit.framework.Assert; importere junit.framework.TestCase; offentlig klasse SingletonTest utvider TestCase {privat statisk loggerlogger = Logger.getRootLogger (); privat statisk Singleton singleton = null; offentlig SingletonTest (strengnavn) {super (navn); } offentlig ugyldig setUp () { singleton = null; } public void testUnique () kaster InterruptedException {// Begge trådene kaller Singleton.getInstance (). Tråd threadOne = ny tråd (ny SingletonTestRunnable ()), threadTwo = ny tråd (ny SingletonTestRunnable ()); threadOne.start ();threadTwo.start (); threadOne.join (); threadTwo.join (); } privat statisk klasse SingletonTestRunnable implementerer Runnable {public void run () {// Få en referanse til singleton. Singleton s = Singleton.getInstance (); // Beskytt singleton-medlemsvariabelen fra // multitrådet tilgang. synkronisert (SingletonTest.class) {if (singleton == null) // Hvis lokal referanse er null ... singleton = s; // ... sett den til singleton} // Lokal referanse må være lik den ene og // eneste forekomsten av Singleton; Ellers har vi to // Singleton-forekomster. Assert.assertEquals (true, s == singleton); } } }

Eksempel 5s testtilfelle oppretter to tråder, starter hver og venter på at de skal fullføres. Test tilfellet opprettholder en statisk referanse til en enkelt forekomst, og hver tråd anrop Singleton.getInstance (). Hvis den statiske medlemsvariabelen ikke er satt, setter den første tråden den til singleton som er oppnådd med anropet til getInstance (), og den statiske medlemsvariabelen sammenlignes med den lokale variabelen for likhet.

Her er hva som skjer når testsaken kjører: Den første tråden ringer getInstance (), går inn i hvis blokkerer, og sover. Deretter ringer også den andre tråden getInstance () og oppretter en singleton-forekomst. Den andre tråden setter deretter den statiske medlemsvariabelen til forekomsten den opprettet. Den andre tråden sjekker den statiske medlemsvariabelen og den lokale kopien for likhet, og testen består. Når den første tråden våkner, oppretter den også en singleton-forekomst, men den tråden setter ikke den statiske medlemsvariabelen (fordi den andre tråden allerede har satt den), så den statiske variabelen og den lokale variabelen er ute av synkronisering, og testen for likhet mislykkes. Eksempel 6 viser eksempler på test 5:

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