Java Savjet 67: Lijena instancija

Nedavno nas je oduševila mogućnost da ugrađena memorija u 8-bitnom mikroračunalu skoči s 8 KB na 64 KB. Sudeći prema sve većim, resursima gladnim aplikacijama koje sada koristimo, nevjerojatno je da je itko ikad uspio napisati program koji se uklapa u tu sićušnu količinu memorije. Iako danas imamo puno više memorije, s tehnikama uspostavljenim za rad u tako strogim ograničenjima možemo naučiti neke vrijedne lekcije.

Štoviše, Java programiranje nije samo pisanje apleta i aplikacija za postavljanje na osobna računala i radne stanice; Java je snažno prodrla i na tržište ugrađenih sustava. Trenutni ugrađeni sustavi imaju relativno oskudne memorijske resurse i računarsku snagu, pa su se mnogi stari problemi s kojima se suočavaju programeri pojavili za programere Java koji rade na području uređaja.

Balansiranje ovih čimbenika fascinantan je problem dizajna: Važno je prihvatiti činjenicu da nijedno rješenje na području ugrađenog dizajna neće biti savršeno. Dakle, moramo razumjeti vrste tehnika koje će biti korisne za postizanje fine ravnoteže potrebne za rad u okviru ograničenja platforme za implementaciju.

Jedna od tehnika očuvanja memorije koju Java programeri smatraju korisnom je lijena instancija. Uz lijenu instanciju, program se suzdržava od stvaranja određenih resursa sve dok resurs prije ne bude potreban - oslobađanje dragocjenog memorijskog prostora. U ovom savjetu ispitujemo tehnike lijene instancije u učitavanju i stvaranju objekata Java klase te posebna razmatranja potrebna za Singleton uzorke. Materijal ovog savjeta potječe iz djela u 9. poglavlju naše knjige Java u praksi: Stilovi dizajna i idiomi za učinkovitu Javu (vidi Resurse).

Željno protiv lijene instancije: primjer

Ako ste upoznati s Netscapeovim web preglednikom i koristili ste obje verzije 3.x i 4.x, nesumnjivo ste primijetili razliku u načinu učitavanja Java runtimea. Ako pogledate početni zaslon kad se Netscape 3 pokrene, primijetit ćete da on učitava razne resurse, uključujući Javu. Međutim, kada pokrenete Netscape 4.x, on ne učitava Java runtime - on čeka dok ne posjetite web stranicu koja uključuje oznaku. Ova dva pristupa ilustriraju tehnike željne instancije (učitajte je u slučaju da je potrebna) i lijene instancije (pričekajte dok se ne zatraži prije nego što je učitate, jer možda nikada neće trebati).

Postoje oba pristupa: s jedne strane, uvijek učitavanje resursa potencijalno troši dragocjenu memoriju ako se resurs ne koristi tijekom te sesije; s druge strane, ako nije učitan, cijenu plaćate u smislu vremena učitavanja kada je resurs prvi put potreban.

Lijenu instanciju smatrajte politikom očuvanja resursa

Lijena instancija u Javi podijeljena je u dvije kategorije:

  • Lijeno učitavanje klase
  • Lijeno stvaranje predmeta

Lijeno učitavanje klase

Java runtime ima ugrađenu lijenu instanciju za klase. Klase se učitavaju u memoriju samo kad se na njih prvi put referencira. (Oni se također mogu prvo učitati s web poslužitelja putem HTTP-a.)

MyUtils.classMethod (); // prvi poziv metodi statičke klase Vector v = new Vector (); // prvi poziv operateru new

Lijeno učitavanje klase važna je značajka Java runtime okruženja jer u određenim okolnostima može smanjiti upotrebu memorije. Na primjer, ako se dio programa nikada ne izvrši tijekom sesije, klase na koje se upućuje samo u tom dijelu programa nikada se neće učitati.

Lijeno stvaranje predmeta

Lijeno stvaranje predmeta usko je povezano s lijenim učitavanjem klase. Prvi put kada novu ključnu riječ upotrijebite za vrstu klase koja prethodno nije bila učitana, Java runtime će je učitati umjesto vas. Lijeno stvaranje predmeta može smanjiti upotrebu memorije u mnogo većoj mjeri od lijenog učitavanja klase.

Da predstavimo koncept lijenog stvaranja objekata, pogledajmo jednostavan primjer koda gdje a Framekoristi a MessageBoxza prikaz poruka o pogreškama:

javna klasa MyFrame proširuje Frame {private MessageBox mb_ = new MessageBox (); // privatni pomoćnik koji koristi ova klasa private void showMessage (String message) {// postavlja tekst poruke mb_.setMessage (message); mb_.pack (); mb_.show (); }}

U gornjem primjeru, kada MyFramese kreira MessageBoxinstanca, stvara se i instanca mb_. Ista pravila vrijede rekurzivno. Dakle, bilo koje varijable instance inicijalizirane ili dodijeljene u MessageBoxkonstruktoru klase također se dodjeljuju iz hrpe i tako dalje. Ako se instanca MyFramene koristi za prikaz poruke o pogrešci unutar sesije, nepotrebno trošimo memoriju.

U ovom prilično jednostavnom primjeru zapravo nećemo dobiti previše. Ali ako uzmete u obzir složeniju klasu, koja koristi mnoge druge klase, koje pak rekurzivno koriste i instanciraju više objekata, potencijalna upotreba memorije je očiglednija.

Lijenu instanciju smatrajte politikom za smanjenje potreba za resursima

Lijeni pristup gore navedenom primjeru naveden je u nastavku, gdje object mb_je instancirano na prvom pozivu na showMessage(). (Odnosno, dok program zapravo ne zatreba.)

javna završna klasa MyFrame proširuje Frame {private MessageBox mb_; // null, implicitni // privatni pomoćnik koji koristi ova klasa private void showMessage (String message) {if (mb _ == null) // prvi poziv na ovu metodu mb_ = new MessageBox (); // postavljanje teksta poruke mb_.setMessage (poruka); mb_.pack (); mb_.show (); }}

Ako bolje pogledate showMessage(), vidjet ćete da prvo utvrđujemo je li varijabla instance mb_ jednaka nuli. Kako nismo inicijalizirali mb_ u njegovoj točki deklaracije, Java runtime se pobrinuo za nas. Dakle, možemo sigurno nastaviti stvaranjem MessageBoxinstance. Svi budući pozivi na showMessage()utvrdit će da mb_ nije jednako nuli, stoga preskakanje stvaranja objekta i korištenje postojeće instance.

Primjer iz stvarnog svijeta

Ispitajmo sada realističniji primjer, gdje lijena instancija može igrati ključnu ulogu u smanjenju količine resursa koje program koristi.

Pretpostavimo da nas je klijent zamolio da napišemo sustav koji će korisnicima omogućiti katalogizaciju slika u datotečnom sustavu i pružiti mogućnost pregleda minijatura ili cjelovitih slika. Naš prvi pokušaj mogao bi biti pisanje klase koja učitava sliku u svoj konstruktor.

javna klasa ImageFile {privatni niz ime_datoteke_; privatna slika image_; javna datoteka datoteka (niz datoteka) {ime_datoteke = ime datoteke; // učitavanje slike} javni niz getName () {return filename_;} javna slika getImage () {return image_; }}

U gornjem primjeru ImageFileimplementira pristup pretjerane instancije Imageobjekta. U svoju korist, ovaj dizajn jamči da će slika biti dostupna odmah u trenutku poziva getImage(). Međutim, ovo ne samo da može biti bolno sporo (u slučaju direktorija koji sadrži mnogo slika), već bi ovaj dizajn mogao iscrpiti dostupnu memoriju. Da bismo izbjegli ove potencijalne probleme, možemo iskoristiti prednosti trenutnog pristupa radi smanjenog korištenja memorije. Kao što ste mogli pretpostaviti, to možemo postići pomoću lijene instancije.

Evo ažurirane ImageFileklase koja koristi isti pristup kao klasa MyFramesa svojom MessageBoxvarijablom instance:

javna klasa ImageFile {privatni niz ime_datoteke_; privatna slika image_; // = null, implicitna javna ImageFile (string filename) {// samo pohranjuje ime datoteke filename_ = ime datoteke; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// prvi poziv za getImage () // učitavanje slike ...} return image_; }}

U ovoj se verziji stvarna slika učitava samo pri prvom pozivu na getImage(). Dakle, da rezimiramo, ovdje je kompromis da smanjimo ukupnu upotrebu memorije i vrijeme pokretanja, plaćamo cijenu za učitavanje slike prvi put kad se zatraži - uvodeći pogodak u izvedbi u tom trenutku u izvršenju programa. Ovo je još jedan idiom koji odražava Proxyobrazac u kontekstu koji zahtijeva ograničenu upotrebu memorije.

Gore ilustrirana politika lijene instancije dobra je za naše primjere, ali kasnije ćete vidjeti kako se dizajn mora mijenjati u kontekstu više niti.

Lijena instancija pojedinačnih uzoraka u Javi

Pogledajmo sada Singleton obrazac. Evo generičkog oblika u Javi:

javna klasa Singleton {private Singleton () {} static private Singleton instance_ = new Singleton (); statička javna Singleton instanca () {return instance_; } // javne metode}

U generičkoj verziji deklarirali smo i inicijalizirali instance_polje na sljedeći način:

statička konačna instanca Singleton_ = novi Singleton (); 

Readers familiar with the C++ implementation of Singleton written by the GoF (the Gang of Four who wrote the book Design Patterns: Elements of Reusable Object-Oriented Software -- Gamma, Helm, Johnson, and Vlissides) may be surprised that we didn't defer the initialization of the instance_ field until the call to the instance() method. Thus, using lazy instantiation:

public static Singleton instance() { if(instance_==null) //Lazy instantiation instance_= new Singleton(); return instance_; } 

The listing above is a direct port of the C++ Singleton example given by the GoF, and frequently is touted as the generic Java version too. If you already are familiar with this form and were surprised that we didn't list our generic Singleton like this, you'll be even more surprised to learn that it is totally unnecessary in Java! This is a common example of what can occur if you port code from one language to another without considering the respective runtime environments.

For the record, the GoF's C++ version of Singleton uses lazy instantiation because there is no guarantee of the order of static initialization of objects at runtime. (See Scott Meyer's Singleton for an alternative approach in C++ .) In Java, we don't have to worry about these issues.

The lazy approach to instantiating a Singleton is unnecessary in Java because of the way in which the Java runtime handles class loading and static instance variable initialization. Previously, we have described how and when classes get loaded. A class with only public static methods gets loaded by the Java runtime on the first call to one of these methods; which in the case of our Singleton is

Singleton s=Singleton.instance(); 

The first call to Singleton.instance() in a program forces the Java runtime to load the class Singleton. As the field instance_ is declared as static, the Java runtime will initialize it after successfully loading the class. Thus guarantees that the call to Singleton.instance() will return a fully initialized Singleton -- get the picture?

Lazy instantiation: dangerous in multithreaded applications

Using lazy instantiation for a concrete Singleton is not only unnecessary in Java, it's downright dangerous in the context of multithreaded applications. Consider the lazy version of the Singleton.instance() method, where two or more separate threads are attempting to obtain a reference to the object via instance(). If one thread is preempted after successfully executing the line if(instance_==null), but before it has completed the line instance_=new Singleton(), another thread can also enter this method with instance_ still ==null -- nasty!

The outcome of this scenario is the likelihood that one or more Singleton objects will be created. This is a major headache when your Singleton class is, say, connecting to a database or remote server. The simple solution to this problem would be to use the synchronized key word to protect the method from multiple threads entering it at the same time:

synchronized static public instance() {...} 

However, this approach is a bit heavy-handed for most multithreaded applications using a Singleton class extensively, thereby causing blocking on concurrent calls to instance(). By the way, invoking a synchronized method is always much slower than invoking a nonsynchronized one. So what we need is a strategy for synchronization that doesn't cause unnecessary blocking. Fortunately, such a strategy exists. It is known as the double-check idiom.

The double-check idiom

Use the double-check idiom to protect methods using lazy instantiation. Here's how to implement it in Java:

public static Singleton instance() { if(instance_==null) //don't want to block here { //two or more threads might be here!!! synchronized(Singleton.class) { //must check again as one of the //blocked threads can still enter if(instance_==null) instance_= new Singleton();//safe } } return instance_; } 

The double-check idiom improves performance by using synchronization only if multiple threads call instance() before the Singleton is constructed. Once the object has been instantiated, instance_ is no longer ==null, allowing the method to avoid blocking concurrent callers.

Korištenje više niti u Javi može biti vrlo složeno. Zapravo, tema paralelnosti toliko je velika da je Doug Lea o njoj napisao cijelu knjigu: Istodobno programiranje na Javi. Ako ste novi u istodobnom programiranju, preporučujemo vam da nabavite primjerak ove knjige prije nego što krenete u pisanje složenih Java sustava koji se oslanjaju na više niti.