Programmering

Sådan navigerer du i det vildledende enkle Singleton-mønster

Singleton-mønsteret er vildledende simpelt, jævnt og især for Java-udviklere. I denne klassiker JavaWorld artikel demonstrerer David Geary, hvordan Java-udviklere implementerer singletons med kodeeksempler til multithreading, classloaders og serialisering ved hjælp af Singleton-mønsteret. Han slutter med et kig på implementering af singletonregistre for at specificere singletons ved kørsel.

Nogle gange er det hensigtsmæssigt at have nøjagtigt en forekomst af en klasse: vinduesadministratorer, udskriftsspoolere og filsystemer er prototypiske eksempler. Typisk er disse objekttyper - kendt som singletoner - tilgængelige med forskellige objekter i et softwaresystem og kræver derfor et globalt adgangspunkt. Selvfølgelig, lige når du er sikker på, at du aldrig har brug for mere end en forekomst, er det en god indsats, at du ombestemmer dig.

Singleton-designmønsteret løser alle disse bekymringer. Med Singleton design mønster kan du:

  • Sørg for, at der kun oprettes en forekomst af en klasse
  • Giv et globalt adgangspunkt til objektet
  • Tillad flere forekomster i fremtiden uden at påvirke klienterne i en singleton-klasse

Selvom Singleton-designmønsteret - som det fremgår af nedenstående figur - er et af de enkleste designmønstre, præsenterer det en række faldgruber for den uforsigtige Java-udvikler. Denne artikel diskuterer Singleton-designmønsteret og adresserer disse faldgruber.

Mere om Java designmønstre

Du kan læse hele David Gearys Java Design Mønstre kolonner, eller se en liste over JavaWorlds seneste artikler om Java designmønstre. Se "Designmønstre, det store billede"til en diskussion om fordele og ulemper ved at bruge Gang of Four-mønstrene. Vil du have mere? Få Enterprise Java-nyhedsbrevet leveret til din indbakke.

Singleton-mønsteret

I Designmønstre: Elementer af genanvendelig objektorienteret softwarebeskriver Gang of Four Singleton-mønsteret således:

Sørg for, at en klasse kun har en forekomst og giver et globalt adgangspunkt til den.

Figuren nedenfor illustrerer Singleton designmønstrets klassediagram.

Som du kan se, er der ikke meget i Singleton-designmønsteret. Singletons opretholder en statisk henvisning til den eneste singletoninstans og returnerer en reference til den instans fra en statisk forekomst () metode.

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

Eksempel 1. Den klassiske singleton

offentlig klasse ClassicSingleton {privat statisk ClassicSingleton-forekomst = null; beskyttet ClassicSingleton () {// Eksisterer kun for at besejre instantiering. } offentlig statisk ClassicSingleton getInstance () {if (forekomst == null) {forekomst = ny ClassicSingleton (); } returnere instans; }}

Singleton implementeret i eksempel 1 er let at forstå. Det ClassicSingleton klasse opretholder en statisk henvisning til den enlige singleton-forekomst og returnerer denne reference fra den statiske getInstance () metode.

Der er flere interessante punkter vedrørende ClassicSingleton klasse. Først, ClassicSingleton anvender en teknik kendt som doven instantiering at skabe singleton; som et resultat oprettes singleton-forekomsten ikke før getInstance () metode kaldes for første gang. Denne teknik sikrer, at singleton-forekomster kun oprettes, når det er nødvendigt.

For det andet skal du bemærke det ClassicSingleton implementerer en beskyttet konstruktør, så klienter ikke kan instantere ClassicSingleton tilfælde dog kan du blive overrasket over at opdage, at følgende kode er fuldstændig lovlig:

offentlig klasse SingletonInstantiator {public SingletonInstantiator () {ClassicSingleton instance = ClassicSingleton.getInstance (); ClassicSingleton anotherInstance =ny ClassicSingleton (); ... } }

Hvordan kan klassen i det foregående kodefragment - som ikke strækker sig ClassicSingleton-lave en ClassicSingleton eksempel, hvis ClassicSingleton konstruktør er beskyttet? Svaret er, at beskyttede konstruktører kan kaldes med underklasser og af andre klasser i samme pakke. Fordi ClassicSingleton og SingletonInstantiator er i samme pakke (standardpakken), SingletonInstantiator () metoder kan skabe ClassicSingleton tilfælde. Dette dilemma har to løsninger: Du kan lave ClassicSingleton konstruktør privat, så kun ClassicSingleton () metoder kalder det; dog betyder det ClassicSingleton kan ikke underklasseres. Nogle gange er det en ønskelig løsning; i så fald er det en god ide at erklære din singleton-klasse endelig, hvilket gør denne hensigt eksplicit og giver kompilatoren mulighed for at anvende ydeevneoptimeringer. Den anden løsning er at placere din singleton-klasse i en eksplicit pakke, så klasser i andre pakker (inklusive standardpakken) ikke kan instantiere singleton-forekomster.

Et tredje interessant punkt om ClassicSingleton: det er muligt at have flere singleton-forekomster, hvis klasser indlæst af forskellige classloaders har adgang til en singleton. Dette scenario er ikke så langt hentet; for eksempel bruger nogle servletcontainere forskellige classloaders til hver servlet, så hvis to servlets har adgang til en singleton, vil de hver have deres egen forekomst.

For det fjerde, hvis ClassicSingleton implementerer java.io.Serialiserbar interface, kan klassens forekomster serieiseres og deserialiseres. Men hvis du serieliserer et singleton-objekt og derefter deserialiserer objektet mere end én gang, har du flere singleton-forekomster.

Endelig og måske vigtigst eksempel 1 ClassicSingleton klasse er ikke trådsikker. Hvis to tråde - vi kalder dem Tråd 1 og Tråd 2 - kalder ClassicSingleton.getInstance () på samme tid to ClassicSingleton forekomster kan oprettes, hvis tråd 1 forhindres lige efter at den kommer ind i hvis blokering og kontrol gives efterfølgende til tråd 2.

Som du kan se fra den foregående diskussion, selvom Singleton-mønsteret er et af de enkleste designmønstre, er implementering af det i Java alt andet end simpelt. Resten af ​​denne artikel behandler Java-specifikke overvejelser for Singleton-mønsteret, men lad os først tage en kort omvej for at se, hvordan du kan teste dine singleton-klasser.

Test singletoner

I resten af ​​denne artikel bruger jeg JUnit sammen med log4j til at teste singleton-klasser. Hvis du ikke er fortrolig med JUnit eller log4j, skal du se Ressourcer.

Eksempel 2 viser en JUnit testtilfælde, der tester eksempel 1's singleton:

Eksempel 2. En enkelt testtest

import org.apache.log4j.Logger; import junit.framework.Assert; importere junit.framework.TestCase; offentlig klasse SingletonTest udvider TestCase {privat 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 ("... fik singleton:" + sone); logger.info ("får singleton ..."); stwo = ClassicSingleton.getInstance (); logger.info ("... fik singleton:" + stwo); } public void testUnique () {logger.info ("kontrol af singletons for lighed"); Assert.assertEquals (sand, sone == stwo); }}

Eksempel 2's test sag påberåber sig ClassicSingleton.getInstance () to gange og gemmer de returnerede referencer i medlemsvariabler. Det testUnique () metode kontrollerer for at se, at referencerne 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: oprettet singleton: Singleton @ e86f41 [java] INFO main: ... fik singleton: Singleton @ e86f41 [java] INFO main: får singleton... [java] INFO main: ... fik singleton: Singleton @ e86f41 [java] INFO main: kontrol af singletons for lighed [java] Tid: 0,032 [java] OK (1 test)

Som den foregående liste illustrerer, går eksempel 2's enkle test med flyvende farver - de to enkeltreferencer opnået med ClassicSingleton.getInstance () er faktisk identiske; disse referencer blev dog opnået i en enkelt tråd. Det næste afsnit stresstest vores singleton-klasse med flere tråde.

Multithreading overvejelser

Eksempel 1'er ClassicSingleton.getInstance () metoden er ikke trådsikker på grund af følgende kode:

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

Hvis en tråd forhindres på linje 2, før opgaven foretages, eksempel medlem variabel vil stadig være nul, og en anden tråd kan derefter komme ind i hvis blok. I så fald oprettes to forskellige singleton-forekomster. Desværre forekommer dette scenario sjældent og er derfor svært at producere under testningen. For at illustrere denne tråd russisk roulette har jeg tvunget problemet ved at genimplementere eksempel 1's klasse. Eksempel 4 viser den reviderede singleton-klasse:

Eksempel 4. Stak dækket

import org.apache.log4j.Logger; offentlig klasse Singleton {privat statisk Singleton singleton = null; privat statisk loggerlogger = Logger.getRootLogger (); privat statisk boolsk førstTråd = sandt; beskyttet Singleton () {// Eksisterer kun for at besejre instantiering. } offentlig statisk Singleton getInstance () { hvis (singleton == null) {simulateRandomActivity (); singleton = ny Singleton (); } logger.info ("oprettet singleton:" + singleton); retur singleton; } privat statisk ugyldighed simulateRandomActivity() { prøve { hvis (første tråd) {firstThread = false; logger.info ("sovende ..."); // Denne lur skal give den anden tråd nok tid // at komme efter den første tråd.Tråd.strømTråd (). Søvn (50); }} fange (InterruptedException ex) {logger.warn ("Søvn afbrudt"); }}}

Eksempel 4's singleton ligner eksempel 1's klasse, bortset fra at singleton i den foregående liste stabler dækket for at tvinge en multithreading-fejl. Første gang den getInstance () metode kaldes, den tråd, der påberåbte metoden, sover i 50 millisekunder, hvilket giver en anden tråd tid til at ringe getInstance () og opret en ny singleton-instans. Når den sovende tråd vågner, skaber den også en ny singleton-instans, og vi har to singleton-forekomster. Selvom eksempel 4's klasse er konstrueret, stimulerer den den virkelige situation, hvor den første tråd, der kalder getInstance () bliver forhindret.

Eksempel 5 test Eksempel 4's singleton:

Eksempel 5. En test, der mislykkes

import org.apache.log4j.Logger; import junit.framework.Assert; importere junit.framework.TestCase; offentlig klasse SingletonTest udvider TestCase {privat statisk logger-logger = Logger.getRootLogger (); privat statisk Singleton singleton = null; offentlig SingletonTest (strengnavn) {super (navn); } offentlig ugyldig setUp () { singleton = null; } public void testUnique () kaster InterruptedException {// Begge tråde kalder 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 reference til singleton. Singleton s = Singleton.getInstance (); // Beskyt singleton-medlemsvariabel fra // multithreaded-adgang. synkroniseret (SingletonTest.class) {if (singleton == null) // Hvis lokal reference er nul ... singleton = s; // ... sæt det til singleton} // Lokal reference skal være lig med den eneste og // eneste forekomst af Singleton; Ellers har vi to // Singleton-forekomster. Assert.assertEquals (true, s == singleton); } } }

Eksempel 5's testcase opretter to tråde, starter hver og venter på, at de er færdige. Testkassen opretholder en statisk henvisning til en enkelt instans, og hver tråd kalder op Singleton.getInstance (). Hvis den statiske medlemsvariabel ikke er indstillet, indstiller den første tråd den til den singleton, der er opnået med opkaldet til getInstance (), og den statiske medlemsvariabel sammenlignes med den lokale variabel for lighed.

Her er hvad der sker, når testsagen kører: Den første tråd kalder op getInstance (), går ind i hvis blokere og sover. Derefter kalder den anden tråd også getInstance () og opretter en enkelt instans. Den anden tråd indstiller derefter den statiske medlemsvariabel til den forekomst, den oprettede. Den anden tråd kontrollerer den statiske medlemsvariabel og den lokale kopi for lighed, og testen består. Når den første tråd vågner, opretter den også en singleton-forekomst, men den tråd indstiller ikke den statiske medlemsvariabel (fordi den anden tråd allerede har indstillet den), så den statiske variabel og den lokale variabel er ude af synkronisering, og testen for lighed mislykkes. Eksempel 6 lister Eksempel 5's test case output: