Programmering

Java Tips 67: Lat instantiering

Det var ikke så lenge siden at vi var begeistret over muligheten for å ha innebygd minne i en 8-biters mikrocomputerhopp fra 8 KB til 64 KB. Bedømt av de stadig økende, ressurssultne applikasjonene vi nå bruker, er det utrolig at noen noen gang klarte å skrive et program for å passe inn i den lille mengden minne. Selv om vi har mye mer minne å leke med i disse dager, kan det læres noen verdifulle leksjoner fra teknikkene som er etablert for å arbeide innenfor så stramme begrensninger.

Videre handler ikke Java-programmering bare om å skrive appletter og applikasjoner for distribusjon på personlige datamaskiner og arbeidsstasjoner; Java har også gjort sterke inngrep i det innebygde systemmarkedet. Nåværende innebygde systemer har relativt knappe minnesressurser og datakraft, så mange av de gamle problemene som programmerere står overfor har dukket opp igjen for Java-utviklere som jobber i enhetsområdet.

Å balansere disse faktorene er et fascinerende designproblem: Det er viktig å akseptere det faktum at ingen løsning innen innebygd design vil være perfekt. Så vi må forstå hvilke teknikker som vil være nyttige for å oppnå den fine balansen som kreves for å arbeide innenfor begrensningene for distribusjonsplattformen.

En av minnebevaringsteknikkene som Java-programmerere finner nyttige, er lat instantiering. Med lat instantiering avstår et program fra å opprette visse ressurser til ressursen først trengs - noe som frigjør verdifull minneplass. I dette tipset undersøker vi late instantieringsteknikker i Java-klasselasting og oppretting av objekter, og de spesielle hensynene som kreves for Singleton-mønstre. Materialet i dette tipset stammer fra arbeidet i kapittel 9 i boka vår, Java i praksis: Designstiler og idiomer for effektiv Java (se Ressurser).

Ivrig mot lat instantiering: et eksempel

Hvis du er kjent med Netscapes nettleser og har brukt begge versjonene 3.x og 4.x, har du utvilsomt lagt merke til en forskjell i hvordan Java-kjøretiden lastes inn. Hvis du ser på sprutskjermen når Netscape 3 starter opp, vil du merke at den laster inn forskjellige ressurser, inkludert Java. Når du starter Netscape 4.x, laster den imidlertid ikke Java-kjøretiden - den venter til du besøker en webside som inneholder koden. Disse to tilnærmingene illustrerer teknikkene til ivrig instantiering (last den i tilfelle den er nødvendig) og lat instantiering (vent til den blir bedt om før du laster den inn, da det kanskje aldri er nødvendig).

Det er ulemper med begge tilnærminger: På den ene siden kaster man alltid bort verdifullt minne hvis ressursen ikke brukes i løpet av den økten; på den annen side, hvis den ikke er lastet, betaler du prisen når det gjelder lastetid når ressursen først kreves.

Vurder lat instantiering som en ressursbevaringspolitikk

Lat instantiering i Java faller i to kategorier:

  • Lazy class loading
  • Lazy object creation

Lat klasse lasting

Java-kjøretiden har innebygd lat instantiering for klasser. Klasser lastes bare inn i minnet når de først refereres til. (De kan også lastes fra en webserver via HTTP først.)

MyUtils.classMethod (); // første kall til en statisk klassemetode Vector v = new Vector (); // første samtale til operatør ny 

Lazy class loading er en viktig funksjon i Java runtime-miljøet, da det kan redusere minnebruk under visse omstendigheter. For eksempel, hvis en del av et program aldri blir utført i løpet av en økt, vil klasser som bare refereres til i den delen av programmet aldri lastes inn.

Lazy object creation

Lazy object creation er tett koblet til lazy class loading. Første gang du bruker det nye nøkkelordet på en klassetype som tidligere ikke er lastet, vil Java-kjøretiden laste det for deg. Lazy object creation kan redusere minnebruk i mye større grad enn lat klasseinnlasting.

For å introdusere konseptet med oppretting av lat objekt, la oss ta en titt på et enkelt kodeeksempel der a Ramme bruker en Meldingsboks for å vise feilmeldinger:

offentlig klasse MyFrame utvider ramme {private MessageBox mb_ = ny MessageBox (); // privat hjelper brukt av denne klassen privat ugyldig showMessage (strengmelding) {// angi meldingsteksten mb_.setMessage (melding); mb_.pack (); mb_.show (); }} 

I eksemplet ovenfor, når en forekomst av MyFrame er skapt, den Meldingsboks forekomst mb_ opprettes også. De samme reglene gjelder rekursivt. Så enhver forekomstvariabel ble initialisert eller tildelt i klassen MeldingsboksKonstruktøren tildeles også fra dyngen og så videre. Hvis forekomsten av MyFrame brukes ikke til å vise en feilmelding i løpet av en økt, vi kaster bort minne unødvendig.

I dette ganske enkle eksemplet kommer vi egentlig ikke til å tjene for mye. Men hvis du vurderer en mer kompleks klasse, som bruker mange andre klasser, som igjen bruker og instanserer flere objekter rekursivt, er den potensielle minnebruk mer tydelig.

Vurder lat instantiering som en policy for å redusere ressurskravene

Den late tilnærmingen til eksemplet ovenfor er oppført nedenfor, der objekt mb_ blir instansert ved første samtale til Vis melding(). (Det vil si ikke før programmet faktisk trenger det.)

offentlig sluttklasse MyFrame utvider Frame {private MessageBox mb_; // null, implisitt // privat hjelper brukt av denne klassen privat ugyldig showMessage (strengmelding) {if (mb _ == null) // første anrop til denne metoden mb_ = ny MessageBox (); // angi meldingsteksten mb_.setMessage (melding); mb_.pack (); mb_.show (); }} 

Hvis du ser nærmere på Vis melding(), ser du at vi først avgjør om forekomstvariabelen mb_ er lik null. Siden vi ikke har initialisert mb_ på tidspunktet for erklæring, har Java-kjøretiden tatt vare på dette for oss. Dermed kan vi trygt fortsette ved å lage Meldingsboks forekomst. Alle fremtidige samtaler til Vis melding() vil finne at mb_ ikke er lik null, og hopper derfor over opprettelsen av objektet og bruker den eksisterende forekomsten.

Et eksempel fra den virkelige verden

La oss nå undersøke et mer realistisk eksempel, der lat instantiering kan spille en nøkkelrolle for å redusere mengden ressurser som brukes av et program.

Anta at vi er blitt bedt av en klient om å skrive et system som lar brukere katalogisere bilder på et filsystem og gi muligheten til å se enten miniatyrbilder eller komplette bilder. Vårt første forsøk kan være å skrive en klasse som laster inn bildet i konstruktøren.

offentlig klasse ImageFile {private String filnavn_; privat bilde image_; offentlig bildefil (strengfilnavn) {filnavn_ = filnavn; // last inn bildet} public String getName () {return filename_;} public Image getImage () {return image_; }} 

I eksemplet ovenfor, ImageFile implementerer en altfor sterk tilnærming til å sette i gang Bilde gjenstand. Til sin fordel garanterer dette designet at et bilde vil være tilgjengelig umiddelbart når du ringer til getImage (). Imidlertid kan ikke bare dette være smertefullt tregt (i tilfelle en katalog som inneholder mange bilder), men dette designet kan tømme tilgjengelig minne. For å unngå disse potensielle problemene, kan vi bytte ytelsesfordelene ved øyeblikkelig tilgang for redusert minnebruk. Som du kanskje har gjettet, kan vi oppnå dette ved å bruke lat instantiering.

Her er den oppdaterte ImageFile klasse ved hjelp av samme tilnærming som klasse MyFrame gjorde med sin Meldingsboks forekomstvariabel:

offentlig klasse ImageFile {private String filnavn_; privat bilde image_; // = null, implisitt offentlig ImageFile (strengfilnavn) {// lagrer bare filnavnet filnavn_ = filnavn; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// first call to getImage () // load the image ...} return image_; }} 

I denne versjonen lastes det faktiske bildet bare ved første samtale til getImage (). Så for å oppsummere er avveien her at for å redusere den totale minnebruk og oppstartstid, betaler vi prisen for å laste inn bildet første gang det blir bedt om - å introdusere et ytelseshit på det tidspunktet i programmets utførelse. Dette er et annet uttrykk som gjenspeiler Fullmektig mønster i en sammenheng som krever en begrenset bruk av minne.

Politikken for lat instantiering illustrert ovenfor er bra for våre eksempler, men senere vil du se hvordan designet må endres i sammenheng med flere tråder.

Lat instantiering for Singleton-mønstre i Java

La oss nå ta en titt på Singleton-mønsteret. Her er det generiske skjemaet i Java:

offentlig klasse Singleton {privat Singleton () {} statisk privat Singleton forekomst_ = ny Singleton (); statisk offentlig Singleton-forekomst () {returforekomst_; } // offentlige metoder} 

I den generiske versjonen erklærte og initialiserte vi forekomst_ felt som følger:

statisk endelig Singleton-forekomst_ = ny Singleton (); 

Lesere kjent med C ++ implementeringen av Singleton skrevet av GoF (Gang of Four som skrev boka Designmønstre: Elementer av gjenbrukbar objektorientert programvare - Gamma, Helm, Johnson og Vlissides) kan være overrasket over at vi ikke utsetter initialiseringen av forekomst_ feltet til samtalen til forekomst() metode. Dermed, ved hjelp av lat instantiering:

offentlig statisk Singleton-forekomst () {if (forekomst _ == null) // Lazy instantiering-forekomst_ = ny Singleton (); returnere forekomst_; } 

Oppføringen ovenfor er en direkte port av C ++ Singleton-eksemplet gitt av GoF, og blir ofte vist som den generiske Java-versjonen. Hvis du allerede er kjent med dette skjemaet og var overrasket over at vi ikke oppførte vår generiske Singleton slik, vil du bli enda mer overrasket over å høre at det er helt unødvendig i Java! Dette er et vanlig eksempel på hva som kan oppstå hvis du porter koden fra ett språk til et annet uten å ta hensyn til de respektive kjøretidsmiljøene.

For ordens skyld bruker GoFs C ++ - versjon av Singleton lat instantiering fordi det ikke er noen garanti for rekkefølgen for statisk initialisering av objekter ved kjøretid. (Se Scott Meyers Singleton for en alternativ tilnærming i C ++.) I Java trenger vi ikke å bekymre deg for disse problemene.

Den dovne tilnærmingen til å sette i gang en Singleton er unødvendig i Java på grunn av måten Java-kjøretiden håndterer klasselasting og initialisering av variabel statisk forekomst. Tidligere har vi beskrevet hvordan og når klassene blir lastet. En klasse med bare offentlige statiske metoder blir lastet av Java-kjøretiden ved første samtale til en av disse metodene; som i tilfelle av vår Singleton er

Singleton s = Singleton.instance (); 

Den første samtalen til Singleton.instance () i et program tvinger Java-kjøretiden til å laste klassen Singleton. Som feltet forekomst_ er erklært som statisk, vil Java-kjøretiden initialiseres etter at klassen er vellykket. Dermed garanterer at samtalen til Singleton.instance () vil returnere en fullt initialisert Singleton - få bildet?

Lat instantiering: farlig i flertrådede applikasjoner

Å bruke lat instantiering for en konkret Singleton er ikke bare unødvendig i Java, det er rett og slett farlig i sammenheng med flertrådede applikasjoner. Tenk på den dovne versjonen av Singleton.instance () metode, der to eller flere separate tråder prøver å få en referanse til objektet via forekomst(). Hvis en tråd er forhåndsbestemt etter å ha utført linjen hvis (forekomst _ == null), men før den har fullført linjen forekomst_ = ny Singleton (), kan en annen tråd også gå inn i denne metoden med forekomst_ fortsatt == null - ekkel!

Resultatet av dette scenariet er sannsynligheten for at ett eller flere Singleton-objekter blir opprettet. Dette er en stor hodepine når Singleton-klassen din, for eksempel, kobler til en database eller ekstern server. Den enkle løsningen på dette problemet ville være å bruke det synkroniserte nøkkelordet for å beskytte metoden mot flere tråder som kommer inn på samme tid:

synkronisert statisk offentlig forekomst () {...} 

Denne tilnærmingen er imidlertid litt tunghendt for de fleste flertrådede applikasjoner som bruker en Singleton-klasse i stor grad, og forårsaker blokkering av samtidige samtaler til forekomst(). For øvrig er det alltid mye tregere å påkalle en synkronisert metode enn å påkalle en ikke-synkronisert. Så det vi trenger er en strategi for synkronisering som ikke forårsaker unødvendig blokkering. Heldigvis eksisterer en slik strategi. Det er kjent som dobbeltsjekk idiom.

Dobbeltsjekk-idiomet

Bruk ordet med dobbeltsjekk for å beskytte metoder som bruker lat instantiering. Slik implementerer du det i Java:

offentlig statisk Singleton-forekomst () {if (forekomst _ == null) // ikke vil blokkere her {// to eller flere tråder kan være her !!! synkronisert (Singleton.class) {// må sjekke igjen da en av de // blokkerte trådene fremdeles kan komme inn hvis (forekomst _ == null) forekomst_ = ny Singleton (); // sikker}} returnerer forekomst_; } 

Dobbeltsjekk-idiomet forbedrer ytelsen ved å bruke synkronisering bare hvis flere tråder ringer forekomst() før Singleton er konstruert. Når objektet er instantiert, forekomst_ er ikke lenger == null, slik at metoden kan unngå å blokkere samtidige innringere.

Å bruke flere tråder i Java kan være veldig komplisert. Faktisk er temaet samtidighet så stort at Doug Lea har skrevet en hel bok om det: Samtidig programmering i Java. Hvis du ikke er kjent med samtidig programmering, anbefaler vi at du får en kopi av denne boken før du begynner å skrive komplekse Java-systemer som er avhengige av flere tråder.

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