Programiranje

Java Nasvet 67: leni primerek

Ne tako dolgo nazaj smo bili navdušeni nad možnostjo, da bi vgrajeni pomnilnik v 8-bitnem mikroračunalniku skočil z 8 KB na 64 KB. Sodeč po vedno večjih aplikacijah, ki jih zdaj potrebujemo, je neverjetno, da je komu uspelo napisati program, ki se prilega tej majhni količini pomnilnika. Čeprav imamo danes veliko več spomina, se lahko iz tehnik, ki delujejo v tako strogih omejitvah, naučimo nekaj dragocenih lekcij.

Poleg tega pri programiranju Java ne gre le za pisanje apletov in aplikacij za razmestitev na osebnih računalnikih in delovnih postajah; Java je močno prodrla tudi na trg vgrajenih sistemov. Trenutni vgrajeni sistemi imajo razmeroma malo pomnilniških virov in računalniške moči, zato so se številne stare težave, s katerimi se soočajo programerji, razvile za razvijalce Java, ki delujejo na področju naprav.

Uravnoteženje teh dejavnikov je fascinanten problem oblikovanja: pomembno je sprejeti dejstvo, da nobena rešitev na področju vgrajenega oblikovanja ne bo popolna. Torej moramo razumeti vrste tehnik, ki bodo koristne pri doseganju natančnega ravnotežja, ki je potrebno za delovanje v okviru omejitev platforme za uvajanje.

Ena izmed tehnik ohranjanja pomnilnika, ki se Java programerjem zdijo koristni, je leni primerek. Z lenobno instantacijo se program vzdrži ustvarjanja določenih virov, dokler vira ne potrebuje prvič - sprostitev dragocenega pomnilniškega prostora. V tem nasvetu preučujemo lene tehnike instanciranja pri nalaganju razreda Java in ustvarjanju predmetov ter posebne premisleke, potrebne za vzorce Singleton. Gradivo v tem nasvetu izhaja iz dela v 9. poglavju naše knjige, Java v praksi: Oblikujte sloge in idiome za učinkovito Java (glej Viri).

Nestrpen in len primer: primer

Če poznate spletni brskalnik Netscape in ste uporabili obe različici 3.x in 4.x, ste nedvomno opazili razliko v nalaganju izvajalnega okolja Java. Če pogledate začetni zaslon, ko se Netscape 3 zažene, boste opazili, da naloži različne vire, vključno z Javo. Ko pa zaženete Netscape 4.x, Java ne naloži izvajalnega okolja - počaka, dokler ne obiščete spletne strani, ki vključuje oznako. Ta dva pristopa ponazarjata tehnike nestrpen primer (naložite ga, če bo potreben) in leni primerek (počakajte, da se zahteva, preden ga naložite, ker morda nikoli ne bo potreben).

Oba pristopa imata pomanjkljivosti: Po eni strani vedno nalaganje vira lahko zapravi dragocen pomnilnik, če se vir med to sejo ne uporablja; po drugi strani pa, če ni naložen, plačate ceno glede na čas nalaganja, ko je vir prvič potreben.

Razmislite o leni instanci kot politiki ohranjanja virov

Leni primerek v Javi spada v dve kategoriji:

  • Leni nalaganje razreda
  • Leno ustvarjanje predmetov

Leni nalaganje razreda

Izvajalno okolje Java ima vgrajen leni primerek za razrede. Razredi se naložijo v pomnilnik šele, ko so prvič navedeni. (Prav tako jih je mogoče najprej naložiti s spletnega strežnika prek HTTP.)

MyUtils.classMethod (); // prvi klic metode statičnega razreda Vector v = new Vector (); // prvi klic operaterju new 

Leno nalaganje razredov je pomembna značilnost okolja za izvajanje Java, saj lahko v določenih okoliščinah zmanjša porabo pomnilnika. Na primer, če se del programa med sejo nikoli ne izvede, se razredi, ki so navedeni samo v tem delu programa, ne bodo nikoli naložili.

Leno ustvarjanje predmetov

Ustvarjanje lenih predmetov je tesno povezano z lenobnim nalaganjem razreda. Ko prvič uporabite novo ključno besedo za razred, ki prej ni bil naložen, jo bo izvajalno okolje Java naloži namesto vas. Ustvarjanje lenih predmetov lahko veliko bolj zmanjša porabo pomnilnika kot lenobno nalaganje razredov.

Da predstavimo koncept lenega ustvarjanja predmetov, si oglejmo preprost primer kode, kjer a Okvir uporablja a MessageBox za prikaz sporočil o napakah:

javni razred MyFrame razširi okvir {private MessageBox mb_ = new MessageBox (); // zasebni pomočnik, ki ga uporablja ta razred private void showMessage (String message) {// nastavi besedilo sporočila mb_.setMessage (message); mb_.pack (); mb_.show (); }} 

V zgornjem primeru, ko primerek MyFrame je ustvarjen, MessageBox ustvari se tudi primerek mb_. Ista pravila veljajo rekurzivno. Vse spremenljivke primerkov so torej inicializirane ali dodeljene v razredu MessageBoxKonstruktorji so prav tako dodeljeni kopici in tako naprej. Če je primer MyFrame se ne uporablja za prikaz sporočila o napaki znotraj seje, po nepotrebnem zapravljamo pomnilnik.

V tem precej preprostem primeru v resnici ne bomo pridobili preveč. Če pa upoštevate bolj zapleten razred, ki uporablja številne druge razrede, ki nato rekurzivno uporabijo in ustvarijo več predmetov, je potencialna poraba pomnilnika bolj očitna.

Razmislite o leni instanci kot politiki za zmanjšanje potreb po virih

Leni pristop k zgornjemu primeru je naveden spodaj, kjer je objekt mb_ je ponazorjen ob prvem klicu showMessage (). (To pomeni, dokler program dejansko ne potrebuje.)

javni končni razred MyFrame razširja Frame {private MessageBox mb_; // null, implicitno // zasebni pomočnik, ki ga uporablja ta razred private void showMessage (String message) {if (mb _ == null) // prvi klic te metode mb_ = new MessageBox (); // nastavimo besedilo sporočila mb_.setMessage (sporočilo); mb_.pack (); mb_.show (); }} 

Če si ga podrobneje ogledate showMessage (), boste videli, da najprej ugotovimo, ali je spremenljivka primerka mb_ enaka nič. Ker mb_ nismo inicializirali na točki deklaracije, je za to poskrbel izvajalni program Java. Tako lahko varno nadaljujemo z ustvarjanjem MessageBox primer. Vsi prihodnji klici na showMessage () bo ugotovil, da mb_ ni enako nič, zato preskočite ustvarjanje predmeta in uporabite obstoječi primerek.

Primer iz resničnega sveta

Zdaj pa preučimo bolj realističen primer, kjer lahko leni primerek igra ključno vlogo pri zmanjševanju količine virov, ki jih program uporablja.

Predpostavimo, da nas je stranka prosila, da napišemo sistem, ki bo uporabnikom omogočil katalogiziranje slik v datotečnem sistemu in omogočil ogled sličic ali celotnih slik. Naš prvi poskus bi lahko bil napisati razred, ki naloži sliko v konstruktor.

javni razred ImageFile {zasebni niz ime_datoteke_; zasebna slika image_; public ImageFile (niz datoteke) {ime_datoteke_ = ime datoteke; // naloži sliko} javni niz getName () {vrni ime_datoteke;;} javna slika getImage () {vrni sliko_; }} 

V zgornjem primeru ImageFile izvaja pristop prekomerne uporabnosti k ustvarjanju primerka Slika predmet. V svojo korist ta oblika zagotavlja, da bo slika na voljo takoj ob klicu getImage (). Vendar ne samo, da bi bilo to lahko boleče počasi (v primeru imenika, ki vsebuje veliko slik), ampak bi ta zasnova lahko izčrpala razpoložljivi pomnilnik. Da bi se izognili tem potencialnim težavam, lahko zamenjamo prednosti takojšnjega dostopa za manjšo porabo pomnilnika. Kot ste že uganili, lahko to dosežemo z uporabo lenega primerka.

Tukaj je posodobljeno ImageFile razred, ki uporablja enak pristop kot razred MyFrame naredil s svojim MessageBox spremenljivka primerka:

javni razred ImageFile {zasebni niz ime_datoteke_; zasebna slika image_; // = null, implicitna javna ImageFile (String filename) {// samo ime datoteke filename_ = ime datoteke; } public String getName () {return ime_datoteke;;} public Image getImage () {if (image _ == null) {// prvi klic v getImage () // naloži sliko ...} return image_; }} 

V tej različici se dejanska slika naloži samo ob prvem klicu getImage (). Če povzamemo, gre tukaj za kompromis, da za zmanjšanje celotne porabe pomnilnika in zagonskih časov plačamo ceno za nalaganje slike ob prvem zahtevanju - uvedbo uspešnosti na tej točki pri izvajanju programa. To je še en idiom, ki odraža Zastopnik vzorec v kontekstu, ki zahteva omejeno uporabo pomnilnika.

Zgoraj prikazana politika lenega primerka je v redu za naše primere, kasneje pa boste videli, kako se mora oblikovanje spremeniti v kontekstu več niti.

Leni primerek za Singleton vzorce v Javi

Oglejmo si zdaj Singleton vzorec. Tu je splošna oblika v Javi:

javni razred Singleton {private Singleton () {} static private Singleton instance_ = new Singleton (); statični javni primer Singleton () {return instance_; } // javne metode} 

V generični različici smo razglasili in inicializirali primer_ polje, kot sledi:

statični končni primer Singletona_ = nov Singleton (); 

Bralci, ki poznajo izvajanje C ++ programa Singleton, ki ga je napisal GoF (Gang of Four, ki je napisal knjigo Vzorci oblikovanja: elementi predmetno usmerjene programske opreme za večkratno uporabo - Gamma, Helm, Johnson in Vlissides) bodo morda presenečeni, da nismo odložili inicializacije primer_ polje do klica primer () metoda. Tako z uporabo lenega primerka:

javni statični primerek Singleton () {if (primer _ == null) // Leni primerek primerka_ = nov Singleton (); vrnitev instance_; } 

Zgornji seznam je neposredno pristanišče primera Singleton C ++, ki ga je navedel GoF, in je pogosto priznan kot generična različica Java. Če že poznate ta obrazec in ste bili presenečeni, da nismo tako našteli našega splošnega Singletona, boste še bolj presenečeni, ko boste izvedeli, da je v Javi popolnoma nepotreben! To je pogost primer, kaj se lahko zgodi, če kodo prenesete iz enega jezika v drugega, ne da bi upoštevali ustrezna okolja izvajanja.

Za zapis GoF-ova različica C ++ Singleton uporablja leni primerek, ker ni zagotovila vrstnega reda statične inicializacije predmetov med izvajanjem. (Za alternativni pristop v jeziku C ++ glejte Singleton Scotta Meyerja.) V Javi nam teh vprašanj ni treba skrbeti.

Leni pristop k instanciranju Singletona je v Javi nepotreben zaradi načina, na katerega izvajalno okolje Java obravnava nalaganje razreda in inicializacijo spremenljivke statičnega primerka. Prej smo opisali, kako in kdaj se naložijo razredi. Razred z samo javnimi statičnimi metodami se med prvim klicem ene od teh metod izvajalnega okolja Java naloži; kar je v primeru našega Singletona

Singleton s = Singleton.instance (); 

Prvi klic Singleton.instance () v programu prisili runtime Java, da naloži razred Singleton. Kot polje primer_ je razglašen za statičen, ga bo izvajalno okolje Java inicializiralo po uspešnem nalaganju razreda. Tako zagotavlja, da je klic k Singleton.instance () bo vrnil popolnoma inicializiran Singleton - dobite sliko?

Leni primerek: nevaren v večnitnih aplikacijah

Uporaba lenega primerka za konkretni Singleton v Javi ni samo nepotrebna, temveč je v kontekstu večnitnih aplikacij naravnost nevarna. Razmislite o leni različici Singleton.instance () metoda, kjer dve ali več ločenih niti poskušata pridobiti sklic na predmet prek primer (). Če je ena nit izključena po uspešnem izvajanju vrstice če (primer _ == null), vendar preden je zaključil vrstico primer_ = nov Singleton (), lahko v to metodo vnese tudi druga nit z instance_ still == null - grdo!

Rezultat tega scenarija je verjetnost, da bo ustvarjen en ali več enot Singleton. To je velik glavobol, ko se vaš razred Singleton recimo poveže z bazo podatkov ali oddaljenim strežnikom. Preprosta rešitev te težave bi bila uporaba sinhronizirane ključne besede za zaščito metode pred več nitmi, ki jo vstopijo hkrati:

sinhroniziran statični javni primerek () {...} 

Vendar je ta pristop nekoliko zahteven za večino večnitnih aplikacij, ki v veliki meri uporabljajo razred Singleton, s čimer povzročajo blokiranje sočasnih klicev na primer (). Mimogrede, priklic sinhronizirane metode je vedno veliko počasnejši od priklica nesinhronizirane metode. Torej potrebujemo strategijo sinhronizacije, ki ne povzroči nepotrebnega blokiranja. Na srečo takšna strategija obstaja. Znano je kot idiom za dvojno preverjanje.

Idiom dvojnega preverjanja

Uporabite idiom za dvojno preverjanje, da zaščitite metode z lenim primerkom. Evo, kako jo implementirati v Javo:

javni statični primerek Singleton () {if (primer _ == null) // tukaj ne želim blokirati {// tukaj sta lahko dve ali več niti !!! sinhronizirano (Singleton.class) {// mora še enkrat preveriti, saj lahko ena od // blokiranih niti še vedno vstopi, če (primer _ == null) instance_ = new Singleton (); // safe}} return instance_; } 

Idiom dvojnega preverjanja izboljša zmogljivost z uporabo sinhronizacije le, če pokliče več niti primer () preden je Singleton zgrajen. Ko je objekt instantiran, primer_ ni več == null, ki omogoča, da se metoda izogne ​​blokiranju sočasnih klicateljev.

Uporaba več niti v Javi je lahko zelo zapletena. Pravzaprav je tema sočasnosti tako velika, da je Doug Lea o njej napisal celo knjigo: Sočasno programiranje v Javi. Če sočasno uporabljate sočasno programiranje, vam priporočamo, da prejmete izvod te knjige, preden se lotite pisanja zapletenih sistemov Java, ki temeljijo na več nitih.

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