JavaSvet - otvorena java zajednica

JavaSvet on Facebook
 
glavna stranica arr2javasvet  english version arr2java.net

Singleton

Igor Spasić
11 Okt 2004

Definicija

Singleton patern obezbeđuje da klasa ima jednu i samo jednu instancu i definiše globalnu tačku pristupa toj instanci.

Opis

U procesu modelovanja aplikacija često se prepoznaju entiti čijih instanci ne treba ili čak ne sme da bude više od jedne. Za takve slučajeve se koristi Singleton patern, pošto on obezbeđuje postojanje samo jedne instance neke klase.

Iako jednostavan, Singleton je jedan od paterna koji je teže obezbediti u Javi, zbog njenog bogatog, enterprajz okruženja. Biće opisani neki načini realizacije Singleton paterna. U daljem tekstu se svesno pravi greška kada se spominje pojam "Singleton": uglavnom se misli na objekat koji sme biti instanciran samo jednom, iako termin "Singleton" označava patern, koga mogu činiti više klasa.

Instanciranje restrikcijom

Ovo je krajnje jednostavna implementacija Singletona koja ne dozvoljava kreiranje više od jedne instance, i to tako što ih broji.

public class Singleton1 {
   
private static int objectCount = 0;
   
public Singleton1() {
       
if (objectCount > 0) {
           
throw new RuntimeException("class already instantiated!");
       
}
       
objectCount++;
   
}
}

Svaki put kada se instancira klasa, proverava se statični brojač instanci i ne dozvoljava se više od jedne. Ovo je ujedno i zanimljiv primer kako se može ostvariti brojanje instanci neke klase, pri čemu bi brojač trebalo dekrementirati kada Garbage Collector pokupi slobodne instance.

Ipak, ovako instancirani Singletoni ne zadovoljava sasvim definiciju: ne postoji tačka pristupa kreiranoj instanci, te je ostavljeno korisniku da vodi računa o tome da ne "izgubi" Singleton objekat.

Instanciranje prvim pristupom

Uobičajen način instanciranja Singletona je njegovo instanciranje na nekoj statičkoj lokaciji, najčešće u okviru same klase:

public class Singleton2 {
   
public static Singleton2 instance = new Singleton2();
   
public Singleton2() {}
   
public void foo() {
       
System.out.println("foo");
   
}
}

Korišćenje se obavlja preko atributa klase:

Singleton2.instance.foo();

Prvi put kada se u programu upotrebi ovaj Singleton, kreiraće se njegova instanca, jer se prilikom učitavanja klase inicijalizuju svi njeni statički atributi. Prva upotreba, dakle, ujedno i instancira objekat. Međutim, nekada je bitno da se Singleton instancira ranije, pre njegove konkretne upotrebe. U takvom slučaju postoji jednostavno rešenje koje ne zahteva bilo kakvu izmenu koda: dosta je pozovati Singleton2.getClass() na mestu gde se želi inicijalizovati ovaj Singleton.

U ovoj realizaciji atribut instance je vidljiv spolja, te je moguće reinstancirati Singleton objekat ako je potrebno. Za slučaj kada to treba sprečiti, dovoljno je statičku referencu označiti kao final:

public static final Singleton2 INSTANCE = new Singleton2();

Ovakvu instancu Singletona nije nikako moguće zameniti nekom drugom (reinstancirati ga). Alternativno, moguće je deklarisati konstruktor klase kao private. Klasa i dalje ostaje public, ali nije moguće instancirati je spolja. Jedino sama Singleton klasa ima pravo da se instancira:

public class Singleton3 {
   
public static Singleton3 instance = new Singleton3();
   
private Singleton3() {}
   
public void foo() {
       
System.out.println("foo");
   
}
}

Na kraju, često se i atribut deklariše kao private, a uvodi novi getter metod kojim se vraća instanca objekta. Sve dok ne postoji adekvatna setter metoda, ovaj Singleton nije moguće reinstancirati:

public class Singleton4 {
   
private static Singleton4 instance = new Singleton4();
   
private Singleton4() {}
   
public static Singleton4 getInstance() {
       
return instance;
   
}
   
public void foo() {
       
System.out.println("foo");
   
}
}

Instanciranje korišćenjem Factory paterna

Mesto instanciranja i mehanizam kontrole broja instanci ne moraju da budu u samoj klasi objekta čija se samo jedna instanca zahteva. Umesto toga može da se koristiti Factory pattern koji vodi računa o instancama, kreira ako ne postoje i vraća uvek samo jednu. Korišćenjem Factory paterna se broj instanci može ograničiti i na neki različit od jedan.

Ovakav Factory bi isto mogao da bude realizovan kao Singleton koji bi imao public metode koja kontrolišu broj, instanciraju i vraćaju druge Singleton objekte. Pristupanjem Factory objektu preko izdvojenog interfejsa, moguće je zaštiti konkretan Factory objekat i tako obezbediti mesto kreiranja instanci.

Instanciranje na osnovu ključa

Kao mehanizam za regulisanje postojanja samo jedne instance može da posluži neka postojeća java klasa (kolekcija) koja u sebi već ima implementiran sličan mehanizam. Tako je mogući koristiti HashMap kao kolekciju koja čuva instance Singletona. Wrapper oko HashMap bi za zadati ključ proveravao da li u internoj HashMap kolekciji postoji objekat. Ako ne, instancira se, a ako postoji, vraća se referenca. Wrapper objekat je ovde isto Factory, kao i u prethodnoj tački.

"Lenjo" instanciranje

Kada se Singleton instancira na nekoj statičkoj lokaciji, bilo kakav prvi pristup klasi paterna učiniće da se kreira i instanca objekta kojeg treba da bude samo jedna istanca. Prvi pristup klasi ne mora nužno da bude poziv metode Singleton objekta. Može se zamisliti primer u kome neki Factory Singleton ojekata sadrži i neke druge metode, koje nemaju nikakve veze sa Singletonima. Pri prvom pozivu ovakvog Factory objekta, svi Singletoni će biti instancirani! Ova operacija može da bude zahtevna, Singletoni mogu da zahtevaju značajne resurse, čime se rad programa značajno usporava.

Bolje rešenje bi bilo raditi lazy instanciranje: instanciranje klase pri prvom konkretnom zahtevu za njom, a ne, kao do sad, pri prvom korišćenju klase koja je sadrži:

public class FSingleton {
   
private static OnlyOne onlyOne;
   
public static OnlyOne getOnlyOneInstance() {
       
if (onlyOne == null) {
           
onlyOne = new OnlyOne();
       
}
       
return onlyOne;
   
}
}

Ovakav Factory ne referencira klasu Singletona sve do prvog poziva getOnlyOneInstance() metode. Tada class loader učitava klasu Singletona i ona se instancira. Kako class loader može da učitava klase preko mreže ili sa bilo kojeg drugog mesta, proces učitavanja može da donese dodatno neželjeno kašnjenje, na koje se dodaje i samo vreme instanciranja. Ukoliko je problem da se ovo kašnjenje pojavi na mestu korišćenja Singletona, postoji način kako se ova vremena mogu razdvojiti. Time bi se na mestu gde se Singleton koristi po prvi put čekalo samo na njegovo instanciranje. Dosta je u gornji Factory ubaciti sledeći red:

private Class onlyOneClass = OnlyOne.class;

Sada se učitavanje klase radi na mestu instanciranja Factory objekta, a ne samog Singletona.

Problem sa ovako realizovanim lazy instanciranjem je to što ono nije thread-safe, pa se može desiti da se Singleton instancira više od jednom. Na primer, instanciranje OnlyOne klase iz prethodnog primera može da traje dugo, a da za to vreme neki drugi thread zahteva Singleton. Kako je referenca još uvek null, ulazak u if blok je i dalje dozvoljen. Rešenje je sinhronizacija if bloka (sinhronizacija cele metode bi kočila pristup celoj klasi, ako postoji više sličnih metoda):

public class FSingleton {
   
private static OnlyOne onlyOne;
   
private static Object lock = new Object();
   
public static OnlyOne getOnlyOneInstance() {
       
synchronized(lock) {
           
if (onlyOne == null) {
               
onlyOne = new OnlyOne();
           
}
        }
       
return onlyOne;
   
}
}

Međutim, svaka upotreba sinhronizacije neizbežno koči rad više threadova koji zahtevaju ili pristupaju istom resursu. Zato je predloženo rešenje, poznato kao double-checking zaključavanje:

public static OnlyOne getOnlyOneInstance() {
   
if (onlyOne == null) {
       
synchronized(lock) {
           
if (onlyOne == null) {
               
onlyOne = new OnlyOne();
           
}
        }
    }
   
return onlyOne;
}

Logički gledano, ovo je elegantno rešenje problema. Međutim, dokazano je da kompajler koji koristi standardnu optimizaciju koda može da "pokvari" ovu logiku i da statička referenca dobije vrednost PRE nego što se blok za inicijalizaciju završio. Tada threadovi ne bi ulazili u if blok iako konstruktor još uvek nije završio sa instanciranjem. Prema on-line dokumentima ovo je dokazao izvesni David Bacon, te se više informacija o ovome može naći na mreži. Prema istim izvorima, problem sa double-checking zaključavanjem se rešava uvođenjem još jednog flega, nezavisnog od atributa Singletona.

Pomenuto rešenje (bar na osnovu implementacija koje su nađena na mreži) za double-checking zaključavanje uz pomoć dodatnog flega nije korektno. Na osnovu svega se može zaključiti da bi double-checking zaključavanje trebalo izbegavati. U jako zanimljivom komentaru Đorđa Trifunovića mogu se naći dodatne informacije i rešenja(!) ovog problema.

Statičko instanciranje

I funktor (functor) je na neki način Singleton. Functor predstavlja klasu koja sadrži samo public static metode. Funktor nije baš u skladu sa OOP teorijom, te su stalno aktuelne rasprave na temu da li je on Singleton. U principu, reč je o dva suprotna koncepta u Javi (ili u OOP), iako se sa strane korišćenja čini da je efekat isti.

Korišćenje

Gornji primeri prikazuju kako je moguće realizovati Singleton patern na nekoliko načina i kako male razlike između realizacija mogu da daju rezultate koji se ponašaju značajno različito. Generalni zaključak bi mogao biti da se bi trebalo dovojiti malo više pažnje za izradu Singletona nego što se to na prvi pogled čini.

Primer

Primer je krajnje trivijalan i sadrži navedene primere realizacije Singleton paterna.