Programavimas

„Java“ patarimas 67: tingus akimirksnis

Ne taip seniai mus jaudino perspektyva turėti vidinę atmintį 8 bitų mikrokompiuteriu nuo 8 KB iki 64 KB. Sprendžiant pagal vis didėjančias, resursų ištroškusias programas, kurias dabar naudojame, nuostabu, kad kas nors sugebėjo parašyti programą, kad tilptų į tą mažą atminties kiekį. Nors šiomis dienomis turime žaisti daug daugiau atminties, galima išmokti vertingų pamokų iš metodų, sukurtų dirbti esant tokiems griežtiems apribojimams.

Be to, „Java“ programavimas nėra tik programėlių ir programų, skirtų diegti asmeniniuose kompiuteriuose ir darbo stotyse, rašymas; „Java“ smarkiai įsiveržė ir į įdėtųjų sistemų rinką. Dabartinėse įterptosiose sistemose yra palyginti nedaug atminties išteklių ir skaičiavimo galios, todėl daugelis senų problemų, su kuriomis susiduria programuotojai, vėl atsirado „Java“ kūrėjams, dirbantiems įrenginių srityje.

Šių veiksnių subalansavimas yra patraukli dizaino problema: svarbu pripažinti faktą, kad joks sprendimas įterptojo dizaino srityje nebus tobulas. Taigi, mes turime suprasti technikos tipus, kurie bus naudingi norint pasiekti puikų balansą, reikalingą dirbti laikantis diegimo platformos apribojimų.

Vienas iš „Java“ programuotojų atminties išsaugojimo būdų yra naudingas tingus akimirksnis. Tingiai akimirksniu programa susilaiko nuo tam tikrų išteklių kūrimo, kol ištekliaus pirmiausia nereikia - atlaisvinant vertingos atminties vietos. Šiame patarime mes išnagrinėsime tingų „instantiation“ metodus, susijusius su „Java“ klasės įkėlimu ir objektų kūrimu, ir specialius „Singleton“ modeliams reikalingus aspektus. Šio patarimo medžiaga yra kilusi iš mūsų knygos 9 skyriaus darbo, „Java“ praktikoje: efektyvios „Java“ dizaino stiliai ir idėjos (žr. šaltinius).

Nekantrus ir tingus akimirksnis: pavyzdys

Jei esate susipažinę su „Netscape“ žiniatinklio naršykle ir naudojote abi 3.x ir 4.x versijas, neabejotinai pastebėjote skirtumą, kaip įkeliama „Java“ vykdymo trukmė. Jei pažvelgsite į purslų ekraną, kai paleidžiamas „Netscape 3“, pastebėsite, kad jis įkelia įvairius išteklius, įskaitant „Java“. Tačiau, paleidus „Netscape 4.x“, jis neįkrauna „Java“ vykdymo laiko - jis laukia, kol apsilankysite tinklalapyje, kuriame yra žyma. Šie du požiūriai iliustruoja nekantrus akimirksnis (įkelkite, jei to prireiks) ir tingus akimirksnis (palaukite, kol bus pareikalauta, prieš įkeliant, nes to niekada neprireiks).

Yra trūkumų, susijusių su abiem būdais: Viena vertus, visada pakraunant išteklius gali būti prarasta brangi atmintis, jei resursas nenaudojamas tos sesijos metu; kita vertus, jei jis nebuvo įkeltas, jūs mokate kainą pagal įkėlimo laiką, kai pirmiausia reikalingas išteklius.

Apsvarstykite tingų greitį kaip išteklių išsaugojimo politiką

Tinginys „Java“ programoje skirstomas į dvi kategorijas:

  • Tinginių klasių pakrovimas
  • Tinginio objekto kūrimas

Tinginių klasių pakrovimas

„Java“ vykdymo laikas turi įmontuotą tingų pamoką klasėms. Klasės į atmintį įkeliamos tik tada, kai jos pirmą kartą nurodomos. (Jie taip pat gali būti pirmiausia įkeliami iš žiniatinklio serverio per HTTP.)

„MyUtils.classMethod“ (); // pirmasis iškvietimas į statinės klasės metodą Vektorius v = naujas Vektorius (); // pirmasis skambutis operatoriui naujas 

Tinginių klasių įkėlimas yra svarbi „Java“ vykdymo laiko savybė, nes tam tikromis aplinkybėmis jis gali sumažinti atminties naudojimą. Pvz., Jei programos dalis niekada nevykdoma sesijos metu, tik toje programos dalyje nurodytos klasės niekada nebus įkeltos.

Tinginio objekto kūrimas

Tinginio objekto kūrimas yra glaudžiai susijęs su tinginio klasės krovimu. Pirmą kartą naudodami naują raktinį žodį klasės tipui, kuris anksčiau nebuvo įkeltas, „Java“ vykdymo laikas jį įkels už jus. Tinginio objekto kūrimas gali žymiai sumažinti atminties naudojimą nei tingus klasės įkėlimas.

Norėdami pristatyti tingaus objekto kūrimo sampratą, pažvelkime į paprastą kodo pavyzdį, kur a Rėmas naudoja a Žinučių dėžutė rodyti klaidų pranešimus:

viešoji klasė „MyFrame“ išplečia rėmelį {private MessageBox mb_ = new MessageBox (); // privatus pagalbininkas, kurį naudoja ši klasė private void showMessage (String message) {// nustatykite pranešimo tekstą mb_.setMessage (message); mb_.pakuotė (); mb_.rodyti (); }} 

Ankstesniame pavyzdyje, kai „MyFrame“ yra sukurtas, Žinučių dėžutė taip pat sukurtas egzempliorius mb_. Rekursyviai galioja tos pačios taisyklės. Taigi visi egzempliorių kintamieji inicializuoti ar priskirti klasėje Žinučių dėžutėkonstruktorius taip pat yra paskirtas be kaupo ir pan. Jei egzempliorius „MyFrame“ nenaudojamas klaidos pranešimui per sesiją rodyti, be reikalo eikvojame atmintį.

Šiame gana paprastame pavyzdyje mes tikrai negausime per daug. Bet jei atsižvelgsite į sudėtingesnę klasę, kurioje naudojamos daugybė kitų klasių, kurios savo ruožtu rekursyviai naudoja ir eksponuoja daugiau objektų, galimas atminties naudojimas yra akivaizdesnis.

Apsvarstykite tingų greitį kaip politiką, leidžiančią sumažinti išteklių poreikius

Tingus požiūris į pirmiau pateiktą pavyzdį pateiktas toliau, kur objektas mb_ yra užmezgamas pirmą kartą paskambinus showMessage (). (Tai yra, tik tada, kai to iš tikrųjų reikia programai.)

viešosios finalinės klasės „MyFrame“ prailgina rėmelį {privatus žinučių dėžutė mb_; // null, implicit // privatus pagalbininkas, kurį naudoja ši klasė private void showMessage (String message) {if (mb _ == null) // pirmasis šio metodo iškvietimas mb_ = new MessageBox (); // nustatykite pranešimo tekstą mb_.setMessage (žinutė); mb_.pakuotė (); mb_.rodyti (); }} 

Jei atidžiau pažvelgsite showMessage ()pamatysite, kad pirmiausia nustatome, ar egzemplioriaus kintamasis mb_ yra lygus nuliui. Kadangi mes dar neinicijavome „mb_“ deklaravimo vietoje, „Java“ vykdymo laikas mums tai pasirūpino. Taigi, mes galime saugiai tęsti kurdami Žinučių dėžutė instancija. Visi būsimi skambučiai showMessage () supras, kad mb_ nėra lygus nuliui, todėl praleisdamas objekto kūrimą ir naudodamas esamą egzempliorių.

Realus pavyzdys

Panagrinėkime realistiškesnį pavyzdį, kai tingus akimirksnis gali atlikti svarbų vaidmenį mažinant programos naudojamų išteklių kiekį.

Tarkime, kad klientas mūsų paprašė parašyti sistemą, kuri leistų vartotojams kataloguoti vaizdus failų sistemoje ir suteiks galimybę peržiūrėti miniatiūras arba visus vaizdus. Pirmasis mūsų bandymas gali būti parašyti klasę, kuri įkelia vaizdą į jo konstruktorių.

viešoji klasė „ImageFile“ {privačios eilutės failo pavadinimas_; privatus Vaizdo vaizdas_; public ImageFile (eilutės failo pavadinimas) {filename_ = failo pavadinimas; // įkelkite vaizdą} public String getName () {return filename_;} public Image getImage () {return image_; }} 

Aukščiau pateiktame pavyzdyje „ImageFile“ įgyvendina pernelyg didelį požiūrį į Vaizdas objektas. Savo naudai šis dizainas garantuoja, kad vaizdas bus iškart pasiekiamas skambinant „getImage“ (). Tačiau tai gali būti ne tik skausmingai lėta (jei kataloge yra daug vaizdų), bet ir šis dizainas gali išeikvoti turimą atmintį. Kad išvengtume šių galimų problemų, galime išparduoti momentinės prieigos našumo pranašumus dėl mažesnio atminties naudojimo. Kaip jūs jau spėjote, mes galime tai pasiekti naudodami tingų greitį.

Štai atnaujinta „ImageFile“ klasėje, taikant tą patį metodą kaip ir klasėje „MyFrame“ padarė su ja Žinučių dėžutė egzemplioriaus kintamasis:

viešoji klasė „ImageFile“ {privačios eilutės failo pavadinimas_; privatus Vaizdo vaizdas_; // = null, numanomas viešas „ImageFile“ (eilutės failo pavadinimas) {// saugo tik failo pavadinimą filename_ = failo vardas; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// pirmasis skambutis į getImage () // įkelkite vaizdą ...} return image_; }} 

Šioje versijoje tikrasis vaizdas įkeliamas tik pirmą kartą paskambinus „getImage“ (). Taigi trumpai tariant, kompromisas yra tas, kad norint sumažinti bendrą atminties naudojimą ir paleidimo laiką, mes mokame kainą už vaizdo įkėlimą pirmą kartą, kai to reikalaujama - įvedant našumo hitą tuo programos vykdymo momentu. Tai dar viena idioma, atspindinti Įgaliojimas šabloną kontekste, kuriam reikia riboto atminties naudojimo.

Pirmiau aprašyta tingaus pavyzdžio politika tinka mūsų pavyzdžiams, tačiau vėliau pamatysite, kaip dizainas turi pasikeisti kelių gijų kontekste.

Tingus „Singleton“ modelių „Java“ pavyzdys

Pažvelkime į „Singleton“ modelį. Štai bendroji „Java“ forma:

viešoji klasė Singleton {private Singleton () {} static private Singleton instance_ = new Singleton (); statinė vieša „Singleton“ instancija () {return instance_; } // viešieji metodai} 

Bendrojoje versijoje mes paskelbėme ir inicializavome instancija_ lauką taip:

statinis galutinis Singletono egzempliorius = naujas Singletonas (); 

Skaitytojai, susipažinę su „Singleton“ C ++ įgyvendinimu, parašė GoF (keturių gauja, parašęs knygą Dizaino modeliai: daugkartinio naudojimo objektų programinės įrangos elementai - Gamma, Helmas, Johnsonas ir Vlissidesas) gali nustebti, kad mes neatidėjome instancija_ lauką iki skambučio į instancija() metodas. Taigi, naudojant tingų greitį:

viešasis statinis „Singleton“ egzempliorius () {if (egzempliorius _ == null) // Tingaus egzemplioriaus pavyzdys_ = naujas „Singleton“ (); grąžinti instanciją_; } 

Aukščiau pateiktas sąrašas yra tiesioginis „GoF“ pateikto „C ++ Singleton“ pavyzdžio prievadas, kuris dažnai taip pat reklamuojamas kaip bendroji „Java“ versija. Jei jau esate susipažinę su šia forma ir nustebote, kad mes nepateikėme savo bendrojo „Singleton“, tai dar labiau nustebsite sužinoję, kad „Java“ tai visiškai nereikalinga! Tai yra įprastas pavyzdys, kas gali atsitikti, jei perkelsite kodą iš vienos kalbos į kitą neatsižvelgdami į atitinkamą vykdymo laiką.

Įrašui, GoF C ++ versijos „Singleton“ naudoja tingų greitinimą, nes nėra garantijos, kad objektai inicializuojami vykdymo metu. (Norėdami pamatyti alternatyvų požiūrį C ++, žr. „Scott Meyer Singleton“.) „Java“ neturime jaudintis dėl šių problemų.

Tingus požiūris į „Singleton“ kūrimą „Java“ yra nereikalingas dėl to, kaip „Java“ vykdymo laikas tvarko klasės įkėlimą ir statinių egzempliorių kintamųjų inicijavimą. Anksčiau mes aprašėme, kaip ir kada įkeliamos klasės. Klasė, turinti tik viešus statinius metodus, „Java“ vykdymo metu įkeliama pirmą kartą iškviečiant vieną iš šių metodų; kas mūsų Singletono atveju yra

Singleton s = Singleton.instancija (); 

Pirmasis skambutis Singleton.instance () programoje priverčia „Java“ vykdymo laiką įkelti klasę Singletonas. Kaip laukas instancija_ yra deklaruojamas kaip statinis, „Java“ vykdymo laikas jį inicijuos sėkmingai įkėlus klasę. Taigi garantuojama, kad raginimas Singleton.instance () grąžins visiškai inicializuotą „Singleton“ - gausite vaizdą?

Tingi akimirksniu: pavojinga naudojant įvairias gijas

Tingaus pavyzdžio naudojimas konkrečiam „Singleton“ yra ne tik nereikalingas „Java“, bet ir tiesiog pavojingas daugialypių programų kontekste. Apsvarstykite tingų versiją Singleton.instance () metodas, kai du ar daugiau atskirų gijų bando gauti nuorodą į objektą per instancija(). Jei sėkmingai įvykdžius eilutę, užkirta viena gija jei (egzempliorius _ == null), bet jam dar neužbaigus eilutės instance_ = naujas Singletonas (), kita gija taip pat gali įvesti šį metodą su egzempliorius_ vis dar == nulinis - bjaurus!

Šio scenarijaus rezultatas yra tikimybė, kad bus sukurtas vienas ar daugiau „Singleton“ objektų. Tai yra didelis galvos skausmas, kai jūsų „Singleton“ klasė, tarkime, jungiasi prie duomenų bazės ar nuotolinio serverio. Paprastas šios problemos sprendimas būtų naudoti sinchronizuotą raktinį žodį, kad apsaugotumėte metodą nuo kelių gijų tuo pačiu metu:

sinchronizuota statinė viešoji instancija () {...} 

Tačiau šis požiūris yra šiek tiek sunkus daugeliui daugialypių programų, kurios plačiai naudoja „Singleton“ klasę, todėl blokuoja vienu metu vykstančius skambučius. instancija(). Beje, sinchronizuoto metodo iškvietimas visada yra daug lėtesnis nei nesinchronizuoto. Taigi mums reikia sinchronizavimo strategijos, kuri nesukeltų nereikalingo blokavimo. Laimei, tokia strategija egzistuoja. Tai žinoma kaip dar kartą patikrinkite idiomą.

Dar kartą patikrinkite idiomą

Naudokite dvigubo patikrinimo idiomą, kad apsaugotumėte metodus, naudodamiesi tingiu momentu. Štai kaip tai įgyvendinti „Java“:

public static Singleton instance () {if (instance _ == null) // nenorite čia blokuoti {// čia gali būti dvi ar daugiau gijų !!! sinchronizuotas („Singleton.class“) {// turi patikrinti dar kartą, nes viena iš // užblokuotų gijų vis tiek gali patekti, jei (egzempliorius _ == nulinis) egzempliorius_ = naujas „Singleton“ (); // saugus}} grąžina egzempliorių_; } 

Dvigubo patikrinimo idioma pagerina našumą naudojant sinchronizavimą tik tuo atveju, jei skambina kelios gijos instancija() prieš pastatant „Singleton“. Surinkus objektą, instancija_ nebėra == nulis, leidžiantis užkirsti kelią tuo pačiu metu skambinančių asmenų blokavimui.

Kelių gijų naudojimas „Java“ gali būti labai sudėtingas. Tiesą sakant, lygiagretumo tema yra tokia didžiulė, kad Doug Lea parašė apie ją visą knygą: Lygiagretus programavimas „Java“. Jei dar nesinaudojate vienu metu atliekamu programavimu, rekomenduojame gauti šios knygos kopiją prieš pradedant rašyti sudėtingas „Java“ sistemas, kurios priklauso nuo kelių gijų.