Programovanie

Java Tip 67: Lenivé vytváranie inštancií

Nebolo to tak dávno, čo sme boli nadšení vyhliadkou, že zabudovaná pamäť bude mať v 8-bitovom mikropočítači skok z 8 KB na 64 KB. Súdiac podľa neustále sa zvyšujúcich aplikácií náročných na zdroje, ktoré teraz používame, je úžasné, že sa každému podarilo napísať program, ktorý by sa zmestil do toho malého množstva pamäte. Aj keď máme v dnešnej dobe oveľa viac pamäte na hranie, z techník zavedených pre prácu v tak prísnych obmedzeniach sa dá vyvodiť niekoľko cenných ponaučení.

Programovanie v jazyku Java navyše nie je len o písaní appletov a aplikácií na nasadenie na osobných počítačoch a pracovných staniciach; Spoločnosť Java taktiež silne prenikla na trh vstavaných systémov. Súčasné zabudované systémy majú relatívne obmedzené pamäťové zdroje a výpočtový výkon, takže pre vývojárov Java pracujúcich v oblasti zariadení sa znova objavilo veľa starých problémov, ktorým programátori čelia.

Vyváženie týchto faktorov je fascinujúcim dizajnovým problémom: Je dôležité akceptovať skutočnosť, že žiadne riešenie v oblasti zabudovaného dizajnu nebude dokonalé. Musíme teda pochopiť typy techník, ktoré budú užitočné pri dosahovaní jemnej rovnováhy potrebnej na prácu v rámci obmedzení platformy nasadenia.

Jedna z techník zachovania pamäte, ktorú Java programátori považujú za užitočnú, je lenivá inštancia. Vďaka lenivému vytváraniu inštancií sa program zdrží vytvárania určitých zdrojov, kým ich najskôr nebudete potrebovať - ​​uvoľníte tak cenný pamäťový priestor. V tomto tipe preskúmame techniky línej inštancie pri načítaní triedy Java a vytváraní objektov a špeciálne úvahy potrebné pre vzory Singleton. Materiál v tomto tipe pochádza z práce v kapitole 9 našej knihy, Java v praxi: Dizajn štýlov a idiómov pre efektívnu Javu (pozri zdroje).

Príklad túžby po lenivej inštancii

Ak ste oboznámení s webovým prehliadačom Netscape a používali ste verzie 3.xa 4.x, nepochybne ste si všimli rozdiel v načítaní runtime Java. Ak sa pozriete na úvodnú obrazovku pri spustení aplikácie Netscape 3, všimnete si, že načítava rôzne zdroje vrátane Java. Keď však spustíte Netscape 4.x, nenačíta sa runtime Java - počká, kým navštívite webovú stránku, ktorá obsahuje značku. Tieto dva prístupy ilustrujú techniky nedočkavá inštancia (v prípade potreby ho načítajte) a lenivá inštancia (pred načítaním počkajte, kým sa o to požiada, pretože to nemusí byť nikdy potrebné).

Obidva prístupy majú nevýhody: Na jednej strane vždy načítanie zdroja potenciálne stráca drahocennú pamäť, ak sa prostriedok počas tejto relácie nepoužíva; na druhej strane, ak nebol načítaný, platíte cenu z hľadiska času načítania, keď je zdroj prvýkrát potrebný.

Lenivé inštancie považujte za politiku zachovania zdrojov

Lenivé inštancie v prostredí Java spadajú do dvoch kategórií:

  • Lenivé načítanie triedy
  • Lenivé vytváranie objektov

Lenivé načítanie triedy

Modul runtime Java má zabudované lenivé inštancie pre triedy. Triedy sa načítajú do pamäte, iba keď sa na ne prvýkrát odkazuje. (Môžu sa tiež najskôr načítať z webového servera prostredníctvom protokolu HTTP.)

MyUtils.classMethod (); // prvé volanie metódy statickej triedy Vector v = new Vector (); // prvé volanie operátora nové 

Načítanie triedy Lazy je dôležitou vlastnosťou runtime prostredia Java, pretože za určitých okolností môže znížiť využitie pamäte. Napríklad, ak sa počas relácie nikdy nevykoná časť programu, načítajú sa triedy, na ktoré sa odkazuje iba v tejto časti programu.

Lenivé vytváranie objektov

Vytváranie lenivých objektov je úzko spojené s načítaním lenivej triedy. Pri prvom použití nového kľúčového slova na type triedy, ktorý sa predtým nenačítal, ho runtime Java načíta za vás. Vytváranie lenivých objektov môže znížiť využitie pamäte v oveľa väčšej miere ako načítanie lenivej triedy.

Aby sme predstavili koncept vytvárania lenivých objektov, pozrime sa na jednoduchý príklad kódu, kde a Rám používa a MessageBox na zobrazenie chybových správ:

public class MyFrame extends Frame {private MessageBox mb_ = new MessageBox (); // privátny pomocník používaný touto triedou private void showMessage (reťazcová správa) {// nastaví text správy mb_.setMessage (správa); mb_.pack (); mb_.show (); }} 

Vo vyššie uvedenom príklade, keď inštancia MyFrame je vytvorený, MessageBox tiež sa vytvorí inštancia mb_. Rovnaké pravidlá platia rekurzívne. Takže akékoľvek inštančné premenné inicializované alebo priradené v triede MessageBoxKonštruktér je tiež pridelený z haldy atď. Ak je príklad MyFrame sa nepoužíva na zobrazenie chybovej správy v rámci relácie, zbytočne strácame pamäť.

V tomto dosť jednoduchom príklade naozaj príliš nezískame. Ale ak vezmete do úvahy zložitejšiu triedu, ktorá používa mnoho ďalších tried, ktoré naopak rekurzívne používajú a inštancujú viac objektov, je zjavnejšie potenciálne využitie pamäte.

Lenivé inštancie považujte za politiku znižovania požiadaviek na zdroje

Lenivý prístup k uvedenému príkladu je uvedený nižšie, kde objekt mb_ je uskutočnený pri prvom hovore na číslo showMessage (). (To znamená, až kým to program skutočne nebude potrebovať.)

verejná finálna trieda MyFrame rozširuje rámec {private MessageBox mb_; // null, implicit // // privátny pomocník používaný touto triedou private void showMessage (reťazcová správa) {if (mb _ == null) // prvé volanie tejto metódy mb_ = nový MessageBox (); // nastaviť text správy mb_.setMessage (správa); mb_.pack (); mb_.show (); }} 

Ak sa bližšie pozriete na showMessage (), uvidíte, že najskôr určíme, či sa inštančná premenná mb_ rovná null. Pretože sme neinicializovali mb_ v jeho deklaračnom bode, runtime Java sa o to postaral za nás. Môžeme teda bezpečne pokračovať vytvorením MessageBox inštancia. Všetky budúce hovory do showMessage () zistí, že mb_ sa nerovná null, preto preskočí vytvorenie objektu a použije existujúcu inštanciu.

Príklad zo skutočného sveta

Pozrime sa teraz na realistickejší príklad, keď lenivé vytváranie inštancií môže hrať kľúčovú úlohu pri znižovaní množstva zdrojov používaných programom.

Predpokladajme, že nás klient požiadal, aby sme napísali systém, ktorý používateľom umožní katalogizovať obrázky na súborovom systéme a poskytne zariadenie na prezeranie miniatúr alebo úplných obrázkov. Náš prvý pokus by mohol byť napísať triedu, ktorá načíta obrázok v jeho konštruktore.

verejná trieda ImageFile {súkromný reťazec názov_súboru_; súkromný obrázok obrázok_; public ImageFile (názov súboru reťazca) {názov_súboru_ = názov súboru; // načítaj obrázok} verejný reťazec getName () {návrat nazov_súboru_;} verejný obrázok getImage () {návrat_snimku_; }} 

Vo vyššie uvedenom príklade ImageFile implementuje nadmerný prístup k vytvoreniu inštancie Obrázok objekt. V jeho prospech tento dizajn zaručuje, že obrázok bude okamžite k dispozícii v čase volania na getImage (). Avšak nielen to mohlo byť bolestivo pomalé (v prípade adresára obsahujúceho veľa obrázkov), ale tento dizajn by mohol vyčerpať dostupnú pamäť. Aby sme sa vyhli týmto potenciálnym problémom, môžeme vymeniť výkonové výhody okamžitého prístupu za znížené využitie pamäte. Ako ste už asi uhádli, dosiahneme to pomocou lenivého vytvárania inštancií.

Tu sú aktualizované informácie ImageFile triedy rovnakým prístupom ako trieda MyFrame urobil s jeho MessageBox premenná inštancie:

verejná trieda ImageFile {súkromný reťazec názov_súboru_; súkromný obrázok obrázok_; // = null, implicitný verejný ImageFile (názov súboru reťazca) {// ukladať iba názov súboru názov_súboru = názov súboru; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// first call to getImage () // load the image ...} return image_; }} 

V tejto verzii sa skutočný obrázok načíta iba pri prvom volaní na getImage (). Aby som to pripomenul, kompromisom je, že kvôli zníženiu celkového využitia pamäte a času spustenia platíme cenu za načítanie obrázka pri prvom požiadaní - zavedením výkonnostného zásahu v tomto bode vykonávania programu. Toto je ďalší frazém, ktorý odráža Proxy vzor v kontexte, ktorý vyžaduje obmedzené použitie pamäte.

Politika lenivého vytvárania inštancií znázornená vyššie je pre naše príklady v poriadku, ale neskôr uvidíte, ako sa dizajn musí meniť v kontexte viacerých vlákien.

Lenivé vytvorenie inštancie pre vzory Singleton v Jave

Poďme sa teraz pozrieť na Singletonov vzor. Tu je všeobecná forma v jazyku Java:

verejná trieda Singleton {private Singleton () {} statická súkromná inštancia Singleton_ = nový Singleton (); statická verejná inštancia Singleton () {návratová inštancia_; } // verejné metódy} 

V generickej verzii sme deklarovali a inicializovali inštancia_ nasledovne:

statická konečná inštancia Singleton_ = nový Singleton (); 

Čitatelia oboznámení s implementáciou jazyka Singleton v jazyku C ++, ktorú napísala GoF (skupina štyroch ľudí, ktorá knihu napísala Dizajnové vzory: Prvky opakovane použiteľného objektovo orientovaného softvéru - Gamma, Helm, Johnson a Vlissides) môžu byť prekvapení, že sme neodložili inicializáciu inštancia_ do volania na pole inštancia () metóda. Takže pomocou lenivého inštancie:

public static Singleton instance () {if (instance _ == null) // Lazy instance instance_ = new Singleton (); návratová inštancia_; } 

Výpis uvedený vyššie je priamym portom príkladu C ++ Singleton, ktorý poskytuje GoF, a často sa o ňom hovorí aj ako o generickej verzii Java. Ak ste už oboznámení s týmto formulárom a boli ste prekvapení, že sme náš všeobecný Singleton takto nevymenovali, budete o to viac prekvapení, keď sa dozviete, že v jazyku Java je to úplne zbytočné! Toto je bežný príklad toho, čo sa môže vyskytnúť, ak prenášate kód z jedného jazyka do druhého bez zohľadnenia príslušných runtime prostredí.

Pre záznam, verzia Singletonu GoF v C ++ používa lenivé inštancie, pretože neexistuje záruka poradia statickej inicializácie objektov za behu. (Alternatívny prístup v jazyku C ++ nájdete v publikácii Scott Meyer's Singleton.) V prostredí Java sa týchto problémov nemusíme obávať.

Lenivý prístup k inštancii Singletonu je v Jave zbytočný kvôli spôsobu, akým runtime Java spracováva načítanie triedy a inicializáciu premennej statickej inštancie. Predtým sme opísali, ako a kedy sa triedy načítajú. Triedu, ktorá má iba verejné statické metódy, načíta runtime Java pri prvom volaní jednej z týchto metód; čo v prípade nášho Singletona je

Singleton s = Singleton.instance (); 

Prvá výzva na číslo Singleton.instance () v programe prinúti runtime Java načítať triedu Singleton. Ako pole inštancia_ je deklarovaný ako statický, Java runtime ho inicializuje po úspešnom načítaní triedy. Takto zaručuje, že volanie na Singleton.instance () vráti plne inicializovaný Singleton - získať obrázok?

Lenivá inštancia: nebezpečná vo viacvláknových aplikáciách

Používanie lenivých inštancií pre konkrétny Singleton je nielen v Jave zbytočné, ale v kontexte viacvláknových aplikácií aj vyložene nebezpečné. Zvážte lenivú verziu súboru Singleton.instance () metóda, pri ktorej sa dve alebo viac samostatných vlákien pokúša získať odkaz na objekt pomocou inštancia (). Ak je po úspešnom vykonaní riadku predvoľba jedného vlákna if (inštancia _ == null), ale skôr, ako dokončí riadok instance_ = new Singleton (), iné vlákno môže tiež vstúpiť do tejto metódy pomocou instance_ still == null - hnusné!

Výsledkom tohto scenára je pravdepodobnosť, že sa vytvorí jeden alebo viac objektov Singleton. Toto je veľká bolesť hlavy, keď sa vaša trieda Singleton, napríklad, pripája k databáze alebo vzdialenému serveru. Jednoduchým riešením tohto problému by bolo použitie synchronizovaného kľúčového slova na ochranu metódy pred vstupom viacerých vlákien súčasne:

synchronizovaná statická verejná inštancia () {...} 

Tento prístup je však pre väčšinu viacvláknových aplikácií trochu náročných na použitie triedy Singleton trochu ťažkopádny, čo spôsobuje blokovanie súbežných volaní na inštancia (). Mimochodom, vyvolanie synchronizovanej metódy je vždy oveľa pomalšie ako vyvolanie nesynchronizovanej. Potrebujeme teda stratégiu synchronizácie, ktorá nespôsobí zbytočné blokovanie. Našťastie takáto stratégia existuje. Je známy ako skontrolovať idiom.

Fráza dvojitej kontroly

Pomocou idiómu dvojitej kontroly ochráňte metódy pomocou lenivého vytvárania inštancií. Tu je príklad, ako to implementovať v Jave:

public static Singleton instance () {if (instance _ == null) // nechcem tu blokovať {// môžu byť tu dve alebo viac vlákien !!! synchronized (Singleton.class) {// musí znova skontrolovať, pretože jedno z // blokovaných vlákien môže stále vstúpiť, ak (instance _ == null) instance_ = new Singleton (); // safe}} návrat instance_; } 

Fráza dvojitej kontroly zvyšuje výkon pomocou synchronizácie iba v prípade, že volá viac vlákien inštancia () pred postavením Singletonu. Po vytvorení inštancie objektu inštancia_ už nie je == null, čo umožňuje metóde zabrániť blokovaniu súbežných volajúcich.

Používanie viacerých vlákien v Jave môže byť veľmi zložité. Téma súbežnosti je v skutočnosti taká rozsiahla, že Doug Lea o nej napísal celú knihu: Súbežné programovanie v Jave. Ak ste v súbežnom programovaní nováčikom, odporúčame vám získať kópiu tejto knihy skôr, ako sa pustíte do písania zložitých systémov Java, ktoré sa spoliehajú na viac vlákien.

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