Programování

Java Tip 67: Líné vytváření instancí

Nebylo to tak dávno, co jsme byli nadšeni vyhlídkou, že palubní paměť bude mít 8bitový mikropočítač skok z 8 KB na 64 KB. Soudě podle neustále se zvyšujících aplikací náročných na zdroje, které nyní používáme, je úžasné, že se komukoli podařilo napsat program, který by se vešel do toho malého množství paměti. I když v dnešní době máme mnohem více paměti na hraní, z technik zavedených pro práci v tak přísných omezeních si můžeme vzít některé cenné ponaučení.

Programování Java navíc není jen o psaní appletů a aplikací pro nasazení na osobních počítačích a pracovních stanicích; Java také silně pronikla na trh vestavěných systémů. Současné vestavěné systémy mají relativně vzácné paměťové zdroje a výpočetní výkon, takže mnoho starých problémů, kterým programátoři čelí, se znovu objevilo pro vývojáře Java pracující v oblasti zařízení.

Vyvažování těchto faktorů je fascinujícím designovým problémem: Je důležité přijmout skutečnost, že žádné řešení v oblasti vestavěného designu nebude dokonalé. Musíme tedy pochopit typy technik, které budou užitečné pro dosažení jemné rovnováhy potřebné pro práci v rámci omezení platformy nasazení.

Jednou z technik zachování paměti, kterou Java programátoři považují za užitečnou, je líná instance. Díky líné instanci se program zdrží vytváření určitých zdrojů, dokud není zdroj nejprve potřebný - uvolňuje cenný paměťový prostor. V tomto tipu prozkoumáme techniky líné instance v načítání tříd Java a vytváření objektů a speciální úvahy vyžadované pro vzory Singleton. Materiál v tomto tipu pochází z práce v kapitole 9 naší knihy, Java v praxi: Design Styles & Idioms for Effective Java (viz Zdroje).

Dychtivá vs. líná instance: příklad

Pokud jste obeznámeni s webovým prohlížečem Netscape a používáte obě verze 3.xa 4.x, nepochybně jste si všimli rozdílu v tom, jak je načítán běhový modul Java. Pokud se podíváte na úvodní obrazovku při spuštění Netscape 3, všimnete si, že načítá různé zdroje, včetně Javy. Když však spustíte Netscape 4.x, nenačte běhový modul Java - počká, dokud nenavštívíte webovou stránku obsahující značku. Tyto dva přístupy ilustrují techniky dychtivá instance (v případě potřeby jej načtěte) a líná instance (před načtením počkejte, dokud nebude vyžádán, protože to nemusí být nikdy potřeba).

Oba přístupy mají nevýhody: Na jedné straně vždy načítání zdroje potenciálně plýtvá drahocennou pamětí, pokud se prostředek během dané relace nepoužívá; na druhou stranu, pokud nebyl načten, zaplatíte cenu z hlediska doby načítání, když je zdroj poprvé požadován.

Zvažte líné vytváření instancí jako politiku zachování zdrojů

Líná instance v Javě spadá do dvou kategorií:

  • Lazy načítání třídy
  • Líné vytváření objektů

Lazy načítání třídy

Modul runtime Java má pro třídy zabudované líné vytváření instancí. Třídy se načtou do paměti, pouze když na ně poprvé odkazují. (Mohou být také nejprve načteny z webového serveru pomocí protokolu HTTP.)

MyUtils.classMethod (); // první volání metody statické třídy Vector v = new Vector (); // první volání operátora nové 

Načítání líné třídy je důležitou vlastností běhového prostředí Java, protože za určitých okolností může snížit využití paměti. Například pokud se během relace nikdy nespustí část programu, načtou se třídy, na které se odkazuje pouze v této části programu.

Líné vytváření objektů

Vytvoření líného objektu je pevně spojeno s načítáním líné třídy. Při prvním použití nového klíčového slova na typ třídy, který nebyl dříve načten, jej runtime Java načte za vás. Vytvoření líného objektu může snížit využití paměti v mnohem větší míře než načítání líné třídy.

Abychom představili koncept vytváření líných objektů, podívejme se na jednoduchý příklad kódu, kde a Rám používá a MessageBox k zobrazení chybových zpráv:

public class MyFrame extends Frame {private MessageBox mb_ = new MessageBox (); // private helper used by this class private void showMessage (String message) {// nastavit text zprávy mb_.setMessage (message); mb_.pack (); mb_.show (); }} 

Ve výše uvedeném příkladu, když instance MyFrame je vytvořen, MessageBox instance mb_ je také vytvořena. Stejná pravidla platí rekurzivně. Takže jakékoli proměnné instance inicializované nebo přiřazené ve třídě MessageBoxKonstruktor je také přidělen z hromady atd. Pokud je instance MyFrame nepoužívá se k zobrazení chybové zprávy v rámci relace, zbytečně plýtváme pamětí.

V tomto poměrně jednoduchém příkladu ve skutečnosti příliš nezískáme. Ale pokud vezmete v úvahu složitější třídu, která používá mnoho dalších tříd, které zase rekurzivně používají a instancují více objektů, potenciální využití paměti je zjevnější.

Zvažte línou instanci jako zásadu ke snížení požadavků na zdroje

Líný přístup k výše uvedenému příkladu je uveden níže, kde objekt mb_ je vytvořen při prvním volání na showMessage (). (To znamená, dokud to program skutečně nepotřebuje.)

public final class MyFrame extends Frame {private MessageBox mb_; // null, implicitní // privátní pomocník používaný touto třídou private void showMessage (String message) {if (mb _ == null) // první volání této metody mb_ = new MessageBox (); // nastavit text zprávy mb_.setMessage (zpráva); mb_.pack (); mb_.show (); }} 

Pokud se podíváte blíže showMessage (), uvidíte, že nejprve určíme, zda se proměnná instance mb_ rovná null. Protože jsme neinicializovali mb_ v jeho deklaračním bodě, runtime Java se o to postaral za nás. Můžeme tedy bezpečně pokračovat vytvořením MessageBox instance. Všechna budoucí volání na showMessage () zjistí, že mb_ se nerovná null, proto přeskočí vytvoření objektu a použije existující instanci.

Příklad ze skutečného světa

Podívejme se nyní na realističtější příklad, kde líné vytváření instancí může hrát klíčovou roli při snižování množství zdrojů použitých programem.

Předpokládejme, že jsme byli klientem požádáni o napsání systému, který umožní uživatelům katalogizovat obrázky na souborovém systému a poskytne zařízení k prohlížení miniatur nebo úplných obrázků. Náš první pokus by mohl být napsat třídu, která načte obrázek v jeho konstruktoru.

veřejná třída ImageFile {soukromý řetězec název_souboru_; soukromý obrázek obrázek_; public ImageFile (řetězec název_souboru) {název_souboru_ = název_souboru; // načíst obrázek} veřejný řetězec getName () {návrat název_souboru_;} veřejný obrázek getImage () {návrat obrázek_; }} 

Ve výše uvedeném příkladu ImageFile implementuje overeager přístup k instanci obraz objekt. Ve svůj prospěch tento design zaručuje, že obrázek bude k dispozici okamžitě v době volání na getImage (). Avšak nejen to by mohlo být bolestně pomalé (v případě adresáře obsahujícího mnoho obrázků), ale tento design by mohl vyčerpat dostupnou paměť. Abychom se vyhnuli těmto potenciálním problémům, můžeme obchodovat s výhodami výkonu okamžitého přístupu pro snížení využití paměti. Jak jste asi uhodli, můžeme toho dosáhnout pomocí líné instance.

Zde je aktualizovaný ImageFile třída používající stejný přístup jako třída MyFrame udělal s jeho MessageBox proměnná instance:

veřejná třída ImageFile {soukromý řetězec název_souboru_; soukromý obrázek obrázek_; // = null, implicitní veřejný ImageFile (název souboru řetězce) {// ukládat pouze název souboru název_souboru = název_souboru; } public String getName () {return nazev_souboru_;} public Image getImage () {if (image _ == null) {// první volání getImage () // načíst obrázek ...} návrat image_; }} 

V této verzi je skutečný obrázek načten pouze při prvním volání getImage (). Abychom rekapitulovali, kompromisem je, že za účelem snížení celkového využití paměti a spouštěcích časů platíme cenu za načtení obrázku při prvním požadavku - zavedení výkonu v tomto bodě v provádění programu. Toto je další idiom, který odráží Proxy vzor v kontextu, který vyžaduje omezené využití paměti.

Zásada líné instance znázorněná výše je pro naše příklady v pořádku, ale později uvidíte, jak se návrh musí změnit v kontextu více vláken.

Líné vytváření instancí pro vzory Singleton v Javě

Pojďme se nyní podívat na vzor Singleton. Tady je obecná forma v Javě:

veřejná třída Singleton {private Singleton () {} statická soukromá instance Singleton_ = nový Singleton (); statická veřejná instance Singleton () {návratová instance_; } // veřejné metody} 

V obecné verzi jsme deklarovali a inicializovali instance_ pole takto:

statická konečná instance Singleton_ = nový Singleton (); 

Čtenáři obeznámení s implementací Singletonu v C ++ od GoF (Gang of Four, který knihu napsal Návrhové vzory: Prvky opakovaně použitelného objektově orientovaného softwaru - Gamma, Helm, Johnson a Vlissides) může být překvapen, že jsme neodložili inicializaci instance_ pole do volání na instance() metoda. Takže pomocí líné instance:

public static Singleton instance () {if (instance _ == null) // Lazy instance instance_ = new Singleton (); návrat instance_; } 

Výše uvedený seznam je přímým portem příkladu C ++ Singleton daným GoF a často je nabízen také jako obecná verze Java. Pokud jste již obeznámeni s tímto formulářem a byli jste překvapeni, že jsme takhle neuváděli náš obecný Singleton, budete ještě více překvapeni, když zjistíte, že v Javě je naprosto zbytečný! Toto je běžný příklad toho, co může nastat, pokud přenášíte kód z jednoho jazyka do druhého bez zohlednění příslušných běhových prostředí.

Pro záznam používá verze Singletonu GoF v C ++ línou instanci, protože neexistuje žádná záruka pořadí statické inicializace objektů za běhu. (Alternativní přístup v jazyce C ++ najdete v Singletonu Scotta Meyera.) V Javě se těchto problémů nemusíme obávat.

Líný přístup k vytvoření instance Singletonu je v Javě zbytečný z důvodu způsobu, jakým runtime Java zpracovává načítání tříd a inicializaci proměnné statické instance. Dříve jsme popsali, jak a kdy se třídy načtou. Třída pouze s veřejnými statickými metodami se načte běhovým modulem Java při prvním volání jedné z těchto metod; což je v případě našeho Singletona

Singleton s = Singleton.instance (); 

První volání na Singleton.instance () v programu vynutí běhový modul Java načíst třídu jedináček. Jako pole instance_ je deklarován jako statický, modul runtime Java jej inicializuje po úspěšném načtení třídy. Tak zaručuje, že volání Singleton.instance () vrátí plně inicializovaný Singleton - získat obrázek?

Líná instance: nebezpečná ve vícevláknových aplikacích

Používání líné instance pro konkrétní Singleton je nejen zbytečné v Javě, je to naprosto nebezpečné v kontextu vícevláknových aplikací. Zvažte línou verzi Singleton.instance () metoda, kde se dvě nebo více samostatných vláken pokouší získat odkaz na objekt pomocí instance(). Pokud je po úspěšném provedení řádku preempted jedno vlákno if (instance _ == null), ale než dokončí řádek instance_ = nový Singleton (), může tuto metodu zadat také jiné vlákno pomocí instance_ still == null - ošklivé!

Výsledkem tohoto scénáře je pravděpodobnost, že bude vytvořen jeden nebo více objektů Singleton. Toto je hlavní bolest hlavy, když se vaše třída Singleton, například, připojuje k databázi nebo vzdálenému serveru. Jednoduchým řešením tohoto problému by bylo použití synchronizovaného klíčového slova k ochraně metody před více vlákny, která do ní vstupují současně:

synchronizovaná statická veřejná instance () {...} 

Tento přístup je však pro většinu vícevláknových aplikací využívajících třídu Singleton trochu těžkopádný, což způsobuje blokování souběžných volání na instance(). Mimochodem, vyvolání synchronizované metody je vždy mnohem pomalejší než vyvolání nesynchronizované. Potřebujeme tedy strategii synchronizace, která nezpůsobí zbytečné blokování. Naštěstí taková strategie existuje. To je známé jako zkontrolujte idiom.

Idiom dvojité kontroly

Použijte idiom dvojité kontroly k ochraně metod pomocí líné instance. Zde je postup, jak to implementovat v Javě:

public static Singleton instance () {if (instance _ == null) // nechcete zde blokovat {// mohou být zde dvě nebo více vláken !!! synchronized (Singleton.class) {// musí znovu zkontrolovat, protože jedno z // blokovaných vláken může stále vstoupit, pokud (instance _ == null) instance_ = new Singleton (); // safe}} vrátit instanci_; } 

Idiom dvojité kontroly zlepšuje výkon pomocí synchronizace pouze v případě, že volá více vláken instance() před postavením Singletonu. Po vytvoření instance objektu instance_ již není více == null, což umožňuje metodě zabránit blokování souběžných volajících.

Používání více vláken v Javě může být velmi složité. Ve skutečnosti je téma souběžnosti tak rozsáhlé, že Doug Lea o něm napsal celou knihu: Souběžné programování v Javě. Pokud jste v souběžném programování noví, doporučujeme vám získat kopii této knihy, než se pustíte do psaní složitých systémů Java, které se spoléhají na více vláken.

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