Ohjelmointi

Java-vinkki 67: Laiska instantiation

Se ei ollut niin kauan sitten, että olimme innoissaan mahdollisuudesta saada sisäinen muisti 8-bittisessä mikrotietokoneessa 8 kt: sta 64 kt: iin. Nyt käytettävien jatkuvasti lisääntyvien resurssienhimoisten sovellusten perusteella on hämmästyttävää, että kukaan on koskaan onnistunut kirjoittamaan ohjelman mahtumaan siihen pieneen muistiin. Vaikka meillä on paljon enemmän muistia soittaa näinä päivinä, joitain arvokkaita oppitunteja tekniikoista, jotka on luotu toimimaan niin tiukoissa rajoissa.

Lisäksi Java-ohjelmointi ei tarkoita vain sovelmien ja sovellusten kirjoittamista käyttöönottoa varten henkilökohtaisilla tietokoneilla ja työasemilla; Java on edistynyt vahvasti myös sulautettujen järjestelmien markkinoilla. Nykyisillä sulautetuilla järjestelmillä on suhteellisen niukat muistiresurssit ja laskentateho, joten monet ohjelmoijien kohtaamat vanhat ongelmat ovat nousseet esiin laitteiden alueella työskenteleville Java-kehittäjille.

Näiden tekijöiden tasapainottaminen on kiehtova suunnitteluongelma: On tärkeää hyväksyä tosiasia, että mikään ratkaisu sulautetun suunnittelun alalla ei ole täydellinen. Joten meidän on ymmärrettävä, minkä tyyppiset tekniikat ovat hyödyllisiä saavuttaaksemme hienon tasapainon, joka vaaditaan toimimaan käyttöönottoalustan rajoissa.

Yksi Java-ohjelmoijien mielestä hyödyllinen muistin säilyttämistekniikka on laiska instantiation. Laiskalla ennakoinnilla ohjelma pidättyy luomasta tiettyjä resursseja, ennen kuin resurssia tarvitaan ensin - vapauttaa arvokasta muistitilaa. Tässä vihjeessä tarkastelemme laiskoja instantiation-tekniikoita Java-luokan latauksessa ja objektien luomisessa sekä Singleton-malleihin vaadittavia erityishuomioita. Tämän vihjeen materiaali on peräisin kirjan 9 luvusta, Java käytännössä: Suunnittelutyylit ja sanat tehokkaalle Java: lle (katso Resurssit).

Innokas vs. laiska instantiation: esimerkki

Jos olet perehtynyt Netscapen verkkoselaimeen ja olet käyttänyt molempia versioita 3.x ja 4.x, olet epäilemättä huomannut eron Java-ajonaikaisessa latauksessa. Jos katsot splash-näyttöä, kun Netscape 3 käynnistyy, huomaat, että se lataa useita resursseja, mukaan lukien Java. Kun käynnistät Netscape 4.x: n, se ei kuitenkaan lataa Java-ajonaikaa - se odottaa, kunnes käyt verkkosivulla, joka sisältää tunnisteen. Nämä kaksi lähestymistapaa kuvaavat innokas instantiation (lataa se tarvittaessa) ja laiska instantiation (odota, kunnes sitä pyydetään, ennen kuin lataat sen, koska sitä ei ehkä koskaan tarvita).

Molemmissa lähestymistavoissa on haittoja: Toisaalta resurssin aina lataaminen saattaa kuluttaa arvokasta muistia, jos resurssia ei käytetä istunnon aikana; toisaalta, jos sitä ei ole ladattu, maksat hinnan latausaikana, kun resurssi vaaditaan ensimmäisen kerran.

Pidä laiska instantiation resurssien säästöpolitiikkana

Laiska välitön Java-jako jakautuu kahteen luokkaan:

  • Laiska luokan lataus
  • Laiska esineiden luominen

Laiska luokan lataus

Java-ajonaikaisella on sisäänrakennettu laiska instantiation luokkiin. Luokat latautuvat muistiin vain, kun niihin viitataan ensimmäisen kerran. (Ne voidaan myös ladata ensin Web-palvelimelta ensin HTTP: n kautta.)

MyUtils.classMethod (); // ensimmäinen kutsu staattiseen luokkamenetelmään Vector v = new Vector (); // ensimmäinen puhelu operaattorille uusi 

Laiska luokan lataus on tärkeä ominaisuus Java-ajonaikaisessa ympäristössä, koska se voi vähentää muistin käyttöä tietyissä olosuhteissa. Esimerkiksi, jos osan ohjelmasta ei koskaan suoriteta istunnon aikana, vain siinä ohjelman osassa mainitut luokat eivät koskaan lataudu.

Laiska esineiden luominen

Laiska esineiden luominen liittyy tiukasti laiskaan luokan lataamiseen. Ensimmäistä kertaa, kun käytät uutta avainsanaa luokkatyypissä, jota ei ole aiemmin ladattu, Java-ajonaikainen lataus avaa sen puolestasi. Laiska objektien luominen voi vähentää muistin käyttöä paljon enemmän kuin laiska luokan lataus.

Esittelemme laiskan objektin luomisen käsitteen katsomalla yksinkertaista koodiesimerkkiä, jossa a Kehys käyttää a Viestilaatikko näyttää virheilmoitukset:

public class MyFrame laajentaa Frame {private MessageBox mb_ = new MessageBox (); // tämän luokan käyttämä yksityinen auttaja private void showMessage (String message) {// aseta viestin teksti mb_.setMessage (message); mb_pakkaus (); mb_.show (); }} 

Yllä olevassa esimerkissä, kun Oma kehys on luotu, Viestilaatikko luodaan myös instanssi mb_. Samoja sääntöjä sovelletaan rekursiivisesti. Joten kaikki instanssimuuttujat alustetaan tai määritetään luokassa ViestilaatikkoRakentaja on myös varattu kasan ulkopuolelle ja niin edelleen. Jos esimerkiksi Oma kehys ei käytetä virheilmoituksen näyttämiseen istunnon aikana, tuhlaamme muistia tarpeettomasti.

Tässä melko yksinkertaisessa esimerkissä emme todellakaan tule saamaan liikaa. Mutta jos harkitset monimutkaisempaa luokkaa, joka käyttää monia muita luokkia, jotka puolestaan ​​käyttävät ja ilmentävät enemmän objekteja rekursiivisesti, mahdollinen muistin käyttö on ilmeisempää.

Pidä laiska instantiation politiikkana vähentää resurssien tarvetta

Laiska lähestymistapa yllä olevaan esimerkkiin on lueteltu alla, jossa esine mb_ on instantioitu ensimmäisessä puhelussa showMessage (). (Eli vasta, kun ohjelma sitä todella tarvitsee.)

julkinen loppuluokka MyFrame laajentaa kehystä {yksityinen MessageBox mb_; // null, implisiittinen // tämän luokan käyttämä yksityinen auttaja private void showMessage (String message) {if (mb _ == null) // ensimmäinen kutsu tähän menetelmään mb_ = new MessageBox (); // aseta viestin teksti mb_.setMessage (viesti); mb_pakkaus (); mb_.show (); }} 

Jos katsot tarkemmin showMessage (), huomaat, että määritämme ensin, onko esiintymämuuttuja mb_ yhtä suuri kuin nolla. Koska emme ole alustaneet mb_: tä sen ilmoituskohdassa, Java-ajonaika on huolehtinut tästä meille. Siten voimme turvallisesti edetä luomalla Viestilaatikko ilmentymä. Kaikki tulevat puhelut numeroon showMessage () huomaa, että mb_ ei ole yhtä suuri kuin nolla, joten ohitetaan objektin luominen ja käytetään olemassa olevaa esiintymää.

Todellinen esimerkki maailmasta

Tarkastellaan nyt realistisempaa esimerkkiä, jossa laiska instantiation voi olla avainasemassa vähentämään ohjelman käyttämien resurssien määrää.

Oletetaan, että asiakas on pyytänyt meitä kirjoittamaan järjestelmän, joka antaa käyttäjien luetteloida kuvat tiedostojärjestelmään ja antaa mahdollisuuden tarkastella joko pikkukuvia tai kokonaisia ​​kuvia. Ensimmäinen yrityksemme voi olla kirjoittaa luokka, joka lataa kuvan konstruktoriinsa.

julkinen luokka ImageFile {yksityinen merkkijono tiedostonimi_; yksityinen kuva kuva_; public ImageFile (String-tiedostonimi) {tiedostonimi_ = tiedostonimi; // lataa kuva} public String getName () {return filename_;} public Image getImage () {return image_; }} 

Yllä olevassa esimerkissä Kuvatiedosto toteuttaa ylenpalttisen lähestymistavan Kuva esine. Sen eduksi tämä muotoilu takaa, että kuva on saatavilla heti puhelun soittamisen yhteydessä getImage (). Tämä voi kuitenkin olla tuskallisen hidasta (jos hakemistossa on paljon kuvia), mutta tämä muotoilu voi tyhjentää käytettävissä olevan muistin. Näiden mahdollisten ongelmien välttämiseksi voimme vaihtaa välittömän pääsyn suorituskykyedut pienempään muistin käyttöön. Kuten olet ehkä arvannut, voimme saavuttaa tämän käyttämällä laiska instantiation.

Tässä on päivitetty Kuvatiedosto luokka käyttäen samaa lähestymistapaa kuin luokka MyFrame teki sen kanssa Viestilaatikko esiintymämuuttuja:

julkinen luokka ImageFile {yksityinen merkkijono tiedostonimi_; yksityinen kuva kuva_; // = null, implisiittinen julkinen ImageFile (String-tiedostonimi) {// tallentaa vain tiedostonimen tiedostonimi_ = tiedostonimi; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// ensimmäinen kutsu getImage () // -ohjelmaan kuvan lataaminen ...} return image_; }} 

Tässä versiossa todellinen kuva ladataan vain ensimmäiseen soittoon getImage (). Joten yhteenvetona voidaan todeta, että tässä on kompromissi, että muistin kokonaiskäytön ja käynnistysaikojen lyhentämiseksi maksamme kuvan lataamisen hinnan ensimmäistä kertaa sitä pyytäessä - lisäämällä suorituskykyosuma ohjelman suorittamisen tässä vaiheessa. Tämä on toinen idioma, joka heijastaa Välityspalvelin kuvio kontekstissa, joka vaatii rajoitettua muistin käyttöä.

Edellä kuvattu laiska instantiation -politiikka sopii mainiosti esimerkkeihimme, mutta myöhemmin näet, kuinka suunnittelun on muututtava useiden säikeiden yhteydessä.

Laiska esimerkki Java-Singleton-kuvioista

Katsotaanpa nyt Singleton-mallia. Tässä on yleinen muoto Java-muodossa:

julkinen luokka Singleton {private Singleton () {} staattinen yksityinen Singleton instance_ = uusi Singleton (); staattinen julkinen Singleton-esiintymä () {return instance_; } // julkiset menetelmät} 

Yleisessä versiossa ilmoitimme ja alustimme ilmentymä_ kenttä seuraavasti:

staattinen lopullinen Singleton-instanssi_ = uusi Singleton (); 

Lukijat, jotka ovat perehtyneet GoF: n (kirjan kirjoittaneen Neljän jengin) Singletonin C ++ -toteutukseen Suunnittelumallit: Uudelleenkäytettävien olio-ohjelmistojen elementit - Gamma, Helm, Johnson ja Vlissides) saattaa olla yllättynyt siitä, että emme lykänneet ilmentymä_ -kenttään, kunnes soitetaan ilmentymä() menetelmä. Joten käyttämällä laiska instantiation:

julkinen staattinen Singleton-esiintymä () {if (instance _ == null) // Lazy instantiation instance_ = new Singleton (); palaa ilmentymä_; } 

Yllä oleva luettelo on suora portti GoF: n C ++ Singleton -esimerkistä, ja sitä mainostetaan usein myös yleisenä Java-versiona. Jos olet jo perehtynyt tähän lomakkeeseen ja yllättynyt siitä, että emme listanneet yleistä Singletoniamme näin, tulet hämmästymään vielä enemmän siitä, että tiedät, että se on täysin tarpeeton Java-sovelluksessa! Tämä on yleinen esimerkki siitä, mitä voi tapahtua, jos siirrät koodin kieleltä toiselle ottamatta huomioon ajonaikaisia ​​ympäristöjä.

GoF: n Singletonin C ++ -versio käyttää laiskaa instantiationia, koska ei ole takeita objektien staattisen alustuksen järjestyksestä ajon aikana. (Katso Scott Meyerin Singleton vaihtoehtoisesta lähestymistavasta C ++: ssa.) Java-ohjelmassa meidän ei tarvitse huolehtia näistä asioista.

Laiska lähestymistapa Singletonin ilmentämiseen on Java-tilassa tarpeetonta, koska Java-ajonaika käsittelee luokan latausta ja staattisen instanssimuuttujan alustusta. Aikaisemmin olemme kuvanneet, miten ja milloin luokat ladataan. Luokka, jolla on vain julkisia staattisia menetelmiä, ladataan Java-ajonaikaisesti ensimmäiseen kutsuun johonkin näistä menetelmistä; mikä Singletonin tapauksessa on

Singleton s = Singleton.instanssi (); 

Ensimmäinen puhelu Singleton.instance () pakottaa Java-ajonaikaisen ohjelman lataamaan luokan Singleton. Koska kenttä ilmentymä_ on ilmoitettu staattiseksi, Java-ajonaika alustaa sen luokan onnistuneen lataamisen jälkeen. Näin taataan, että puhelu Singleton.instance () palauttaa täysin alustetun Singletonin - saatko kuvan?

Laiska instantiation: vaarallinen monisäikeisissä sovelluksissa

Laiskan instantiationin käyttäminen konkreettiseen Singletoniin ei ole vain tarpeetonta Java-ohjelmassa, se on suorastaan ​​vaarallista monisäikeisten sovellusten yhteydessä. Harkitse Singleton.instance () menetelmä, jossa vähintään kaksi erillistä säiettä yrittää saada viittauksen kohteeseen ilmentymä(). Jos yksi säie on ennalta suoritettu rivin onnistuneen suorittamisen jälkeen if (esiintymä _ == null), mutta ennen kuin se on saanut rivin valmiiksi instanssi_ = uusi Singleton (), toinen ketju voi myös kirjoittaa tämän menetelmän instanssi_ edelleen == tyhjä - ilkeä!

Tämän skenaarion tulos on todennäköisyys, että yksi tai useampi Singleton-objekti luodaan. Tämä on suuri päänsärky, kun Singleton-luokkasi muodostaa yhteyden esimerkiksi tietokantaan tai etäpalvelimeen. Yksinkertainen ratkaisu tähän ongelmaan olisi käyttää synkronoitua avainsanaa menetelmän suojaamiseksi useilta säikeiltä, ​​jotka tulevat siihen samanaikaisesti:

synkronoitu staattinen julkinen esiintymä () {...} 

Tämä lähestymistapa on kuitenkin hieman raskas useimmille monisäikeisille sovelluksille, jotka käyttävät Singleton-luokkaa laajasti, aiheuttaen samalla eston samanaikaisille puheluille. ilmentymä(). Muuten, synkronoidun menetelmän kutsuminen on aina paljon hitaampaa kuin synkronoimattoman menetelmän kutsuminen. Tarvitsemme siis synkronointistrategian, joka ei aiheuta tarpeetonta estoa. Onneksi tällainen strategia on olemassa. Se tunnetaan nimellä tarkista idiomi.

Tarkista kaksinkertainen tarkennus

Käytä kaksinkertaisen tarkistuksen idioomia suojaamaan menetelmiä, jotka käyttävät laiskaa instantointia. Näin se toteutetaan Java-ohjelmassa:

julkinen staattinen Singleton-instanssi () {if (instanssi _ == null) // et halua estää tätä {// kaksi tai useampia ketjuja saattaa olla täällä !!! synkronoitu (Singleton.class) {// on tarkistettava uudelleen, koska yksi // estetyistä säikeistä voi silti tulla, jos (instanssi _ == null) ilmentymä_ = uusi Singleton (); // turvallinen}} palautusilma_; } 

Kaksinkertaisen tarkistuksen idioomi parantaa suorituskykyä käyttämällä synkronointia vain, jos useita ketjuja kutsutaan ilmentymä() ennen Singletonin rakentamista. Kun objekti on instantisoitu, ilmentymä_ ei ole enää == nolla, mikä sallii menetelmän estää samanaikaisten soittajien estämisen.

Useiden ketjujen käyttö Java-ohjelmassa voi olla hyvin monimutkaista. Itse asiassa samanaikaisuuden aihe on niin laaja, että Doug Lea on kirjoittanut siitä kokonaisen kirjan: Samanaikainen ohjelmointi Java-ohjelmassa. Jos sinulla on uusi ohjelmointi samanaikaisesti, suosittelemme, että hankit kopion tästä kirjasta, ennen kuin kirjoitat monimutkaisia ​​Java-järjestelmiä, jotka perustuvat useisiin ketjuihin.