Kako se kretati varljivo jednostavnim Singleton uzorkom

Uzorak Singleton varljivo je jednostavan, čak i posebno za programere Java. U ovom klasičnom članku o JavaWorldu , David Geary demonstrira kako programeri Java implementiraju singletone, s primjerima koda za multithreading, učitavanje klasa i serializaciju pomoću Singleton uzorka. Zaključuje osvrtom na implementaciju jednostrukih registara kako bi se odredile jednokrevetne datoteke tijekom izvođenja.

Ponekad je prikladno imati točno jedan primjerak klase: upravitelji prozora, spuleri ispisa i datotečni sustavi prototipski su primjeri. Tipičnim objektima - poznatim kao singletoni - obično pristupaju različiti objekti u cijelom softverskom sustavu i zato im je potrebna globalna točka pristupa. Naravno, taman kad ste sigurni da vam nikada neće trebati više od jedne instance, dobra je oklada da ćete se predomisliti.

Uzorak dizajna Singleton rješava sve ove zabrinutosti. Pomoću uzorka dizajna Singleton možete:

  • Osigurajte da je stvoren samo jedan primjerak klase
  • Osigurajte globalnu pristupnu točku objektu
  • Dopustite više instanci u budućnosti bez utjecaja na klijente pojedinačne klase

Iako je uzorak dizajna Singleton - što dolje pokazuje donja slika - jedan od najjednostavnijih uzoraka dizajna, on predstavlja nekoliko zamki za neopreznog programera Javu. Ovaj članak raspravlja o uzorku dizajna Singleton i bavi se tim zamkama.

Više o uzorcima dizajna Java

Možete pročitati sve stupce Java Design Patterns Davida Gearyja ili pogledati popis najnovijih članaka JavaWorlda o uzorcima Java dizajna. Pogledajte " Dizajn uzoraka, velika slika " za raspravu o prednostima i nedostacima korištenja uzoraka Gang of Four. Želite više? Nabavite bilten Enterprise Java dostavljen u pristiglu poštu.

Uzorak Singleton

U Dizajn uzorci: Elementi višestruko objektno orijentiranog softvera , Banda četvero opisuje Singleton obrazac ovako:

Osigurajte da klasa ima samo jednu instancu i omogućite joj globalnu točku pristupa.

Donja slika prikazuje dijagram klase uzorka dizajna Singleton.

Kao što vidite, uzorak dizajna Singleton nema puno. Jednotonci održavaju statičku referencu na jedinu pojedinačnu instancu i vraćaju referencu na tu instancu iz statičke instance()metode.

Primjer 1 prikazuje klasičnu izvedbu uzorka dizajna Singleton:

Primjer 1. Klasični singleton

public class ClassicSingleton { private static ClassicSingleton instance = null; protected ClassicSingleton() { // Exists only to defeat instantiation. } public static ClassicSingleton getInstance() { if(instance == null) { instance = new ClassicSingleton(); } return instance; } }

Jednostavno je implementirano u Primjeru 1 lako razumljivo. ClassicSingletonKlasa održava statički referencu na usamljenog Singleton primjer i vraća tu referencu od statičkog getInstance()metode.

Postoji nekoliko zanimljivih točaka koje se tiču ClassicSingletonrazreda. Prvo, ClassicSingletonkoristi tehniku ​​poznatu kao lijena instancija za stvaranje singletona; kao rezultat, pojedinačna instanca se ne stvara dok se getInstance()metoda ne pozove prvi put. Ova tehnika osigurava da se pojedinačni primjerci kreiraju samo kada je to potrebno.

Drugo, primijetite da ClassicSingletonimplementira zaštićeni konstruktor tako da klijenti ne mogu instancirati ClassicSingletoninstance; no možda ćete se iznenaditi kad otkrijete da je sljedeći kôd potpuno legalan:

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

Kako klasa u prethodnom fragmentu koda - koja se ne proteže - može ClassicSingletonstvoriti ClassicSingletoninstancu ako je ClassicSingletonkonstruktor zaštićen? Odgovor je da se zaštićeni konstruktori mogu pozivati ​​po podrazredima i drugim klasama u istom paketu . Budući da su ClassicSingletoni SingletonInstantiatornalaze se u istom paketu (zadani paket), SingletonInstantiator()metode mogu stvarati ClassicSingletoninstance. Ova dvojba ima dva rješenja: ClassicSingletonkonstruktor možete učiniti privatnim tako da ClassicSingleton()ga pozivaju samo metode; međutim, to se sredstvo ClassicSingletonne može podrazvrstati. Ponekad je to poželjno rješenje; ako je tako, dobra je ideja prijaviti svoju singleton klasufinal, što tu namjeru čini eksplicitnom i omogućuje prevoditelju primjenu optimizacija izvedbe. Drugo rješenje je staviti svoju singleton klasu u eksplicitni paket, tako da klase u drugim paketima (uključujući zadani paket) ne mogu instancirati pojedinačne instance.

Treća zanimljiva stvar u vezi s ClassicSingleton: moguće je imati više pojedinačnih instanci ako klase učitane od strane različitih učitavača klasa pristupaju pojedinačnom. Taj scenarij nije tako pretjeran; na primjer, neki spremnici servleta koriste različite učitavače razreda za svaki servlet, pa ako dva servleta pristupaju pojedinom tonu, svaki će imati svoju instancu.

Četvrto, ako ClassicSingletonimplementira java.io.Serializablesučelje, instance klase mogu se serializirati i deserializirati. Međutim, ako serializirate singleton objekt i nakon toga deserijalizirate taj objekt više puta, imat ćete više pojedinačnih instanci.

Konačno, i možda najvažnije, ClassicSingletonklasa Primjera 1 nije sigurna u nitima. Ako ClassicSingleton.getInstance()se istodobno pozivaju dvije niti - nazvat ćemo ih nit 1 i nit 2 , ClassicSingletonmogu se stvoriti dvije instance ako se nit 1 preduhitri odmah nakon što uđe u ifblok, a kontrola se naknadno dade niti 2.

Kao što možete vidjeti iz prethodne rasprave, iako je Singleton uzorak jedan od najjednostavnijih uzoraka dizajna, njegova primjena u Javi je sve samo ne jednostavna. Ostatak ovog članka bavi se razlozima specifičnim za Javu za Singleton uzorak, ali prvo napravimo kratki zaobilazni put kako bismo vidjeli kako možete testirati svoje singleton klase.

Ispitajte jednotočke

U ostatku ovog članka koristim JUnit u suradnji s log4j za testiranje singleton klasa. Ako niste upoznati s JUnit-om ili log4j, pogledajte Resursi.

Primjer 2 navodi JUnit test slučaj koji testira singleton primjera 1:

Primjer 2. Jednostruki testni slučaj

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) { super(name); } public void setUp() { logger.info("getting singleton..."); sone = ClassicSingleton.getInstance(); logger.info("...got singleton: " + sone); logger.info("getting singleton..."); stwo = ClassicSingleton.getInstance(); logger.info("...got singleton: " + stwo); } public void testUnique() { logger.info("checking singletons for equality"); Assert.assertEquals(true, sone == stwo); } }

Test primjer Primjera 2 poziva se ClassicSingleton.getInstance()dvaput i vraća vraćene reference u varijable člana. Na testUnique()metoda provjere da se vidi da su reference su identični. Primjer 3 pokazuje da je rezultat ispitivanja:

Primjer 3. Izlaz test slučaja

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) compile: run-test-text: [java] .INFO main: getting singleton... [java] INFO main: created singleton: [email protected] [java] INFO main: ...got singleton: [email protected] [java] INFO main: getting singleton... [java] INFO main: ...got singleton: [email protected] [java] INFO main: checking singletons for equality [java] Time: 0.032 [java] OK (1 test)

Kao što ilustrira prethodni popis, jednostavan test Primjera 2 prolazi s lepršavim bojama - dvije pojedinačne reference dobivene s ClassicSingleton.getInstance()doista su identične; međutim, te su reference dobivene u jednoj niti. Sljedeći odjeljak testira stres naše klase singleton s više niti.

Razmatranja višetretnosti

ClassicSingleton.getInstance()Metoda primjera 1 nije sigurna u nitima zbog sljedećeg koda:

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

If a thread is preempted at Line 2 before the assignment is made, the instance member variable will still be null, and another thread can subsequently enter the if block. In that case, two distinct singleton instances will be created. Unfortunately, that scenario rarely occurs and is therefore difficult to produce during testing. To illustrate this thread Russian roulette, I've forced the issue by reimplementing Example 1's class. Example 4 shows the revised singleton class:

Example 4. Stack the deck

import org.apache.log4j.Logger; public class Singleton { private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger(); private static boolean firstThread = true; protected Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; } private static void simulateRandomActivity() { try { if(firstThread) { firstThread = false; logger.info("sleeping..."); // This nap should give the second thread enough time // to get by the first thread.Thread.currentThread().sleep(50); } } catch(InterruptedException ex) { logger.warn("Sleep interrupted"); } } }

Example 4's singleton resembles Example 1's class, except the singleton in the preceding listing stacks the deck to force a multithreading error. The first time the getInstance() method is called, the thread that invoked the method sleeps for 50 milliseconds, which gives another thread time to call getInstance() and create a new singleton instance. When the sleeping thread awakes, it also creates a new singleton instance, and we have two singleton instances. Although Example 4's class is contrived, it stimulates the real-world situation where the first thread that calls getInstance() gets preempted.

Example 5 tests Example 4's singleton:

Example 5. A test that fails

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private static Logger logger = Logger.getRootLogger(); private static Singleton singleton = null; public SingletonTest(String name) { super(name); } public void setUp() { singleton = null; } public void testUnique() throws InterruptedException { // Both threads call 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() { // Get a reference to the singleton. Singleton s = Singleton.getInstance(); // Protect singleton member variable from // multithreaded access. synchronized(SingletonTest.class) { if(singleton == null) // If local reference is null... singleton = s; // ...set it to the singleton } // Local reference must be equal to the one and // only instance of Singleton; otherwise, we have two // Singleton instances. Assert.assertEquals(true, s == singleton); } } }

Example 5's test case creates two threads, starts each one, and waits for them to finish. The test case maintains a static reference to a singleton instance, and each thread calls Singleton.getInstance(). If the static member variable has not been set, the first thread sets it to the singleton obtained with the call to getInstance(), and the static member variable is compared to the local variable for equality.

Evo što se događa kada se test slučaj pokrene: Prva nit pozove getInstance(), uđe u ifblok i spava. Nakon toga, druga nit također poziva getInstance()i kreira pojedinačnu instancu. Druga nit zatim postavlja statičku varijablu člana na instancu koju je kreirala. Druga nit provjerava jednakost statičke varijable člana i lokalne kopije i test prolazi. Kad se prva nit probudi, ona također stvara pojedinačnu instancu, ali ta nit ne postavlja statičku varijablu člana (jer ju je druga nit već postavila), tako da statička varijabla i lokalna varijabla nisu sinkronizirane, a test jer jednakost propada. Primjer 6 navodi rezultate primjera 5 primjera 5: