Programmering

Java Tip 67: Lazy instantiering

Det var ikke så længe siden, at vi var begejstrede over udsigten til at have den indbyggede hukommelse i et 8-bit mikrocomputer-spring fra 8 KB til 64 KB. At dømme efter de stadigt stigende ressource-sultne applikationer, vi nu bruger, er det forbløffende, at nogen nogensinde har formået at skrive et program, der passer til den lille mængde hukommelse. Mens vi har meget mere hukommelse at lege med i disse dage, kan nogle værdifulde lektioner drages af de teknikker, der er etableret til at arbejde inden for så stramme begrænsninger.

Desuden handler Java-programmering ikke kun om at skrive applets og applikationer til implementering på pc'er og arbejdsstationer; Java har også gjort stærke indgreb i markedet for integrerede systemer. Nuværende indlejrede systemer har relativt knappe hukommelsesressourcer og computerkraft, så mange af de gamle problemer, som programmører står over for, er dukket op igen for Java-udviklere, der arbejder i enhedens verden.

At afbalancere disse faktorer er et fascinerende designproblem: Det er vigtigt at acceptere det faktum, at ingen løsning inden for det integrerede design vil være perfekt. Så vi er nødt til at forstå de typer teknikker, der vil være nyttige til at opnå den fine balance, der kræves for at arbejde inden for rammerne af implementeringsplatformen.

En af de hukommelsesbevarende teknikker, som Java-programmører finder nyttige, er doven instantiering. Med doven instantiering afstår et program fra at oprette bestemte ressourcer, indtil ressourcen først er nødvendig - hvilket frigør værdifuld hukommelsesplads. I dette tip undersøger vi dovne instantieringsteknikker i Java-klasseindlæsning og oprettelse af objekter og de specielle overvejelser, der kræves for Singleton-mønstre. Materialet i dette tip stammer fra arbejdet i kapitel 9 i vores bog, Java i praksis: Designstilarter og idiomer til effektiv Java (se Ressourcer).

Ivrig mod doven instantiering: et eksempel

Hvis du er fortrolig med Netscapes webbrowser og har brugt begge versioner 3.x og 4.x, har du utvivlsomt bemærket en forskel i, hvordan Java-runtime indlæses. Hvis du ser på plasmaskærmen, når Netscape 3 starter, vil du bemærke, at den indlæser forskellige ressourcer, herunder Java. Når du starter Netscape 4.x, indlæser den imidlertid ikke Java-runtime - den venter, indtil du besøger en webside, der indeholder tagget. Disse to tilgange illustrerer teknikkerne til ivrig instantiering (indlæs det, hvis det er nødvendigt) og doven instantiering (vent indtil det bliver anmodet om, inden du indlæser det, da det måske aldrig er nødvendigt).

Der er ulemper ved begge tilgange: På den ene side spilder du altid en ressource, der potentielt spilder dyrebar hukommelse, hvis ressourcen ikke bruges under den session; på den anden side, hvis den ikke er blevet indlæst, betaler du prisen i form af indlæsningstid, når ressourcen først kræves.

Overvej doven instantiering som en politik til bevarelse af ressourcer

Lazy instantiering i Java falder i to kategorier:

  • Lazy class loading
  • Lazy object creation

Lazy class loading

Java-runtime har indbygget doven instantiering til klasser. Klasser indlæses kun i hukommelsen, når de først henvises til. (De kan også indlæses fra en webserver via HTTP først.)

MyUtils.classMethod (); // første kald til en statisk klassemetode Vector v = ny Vector (); // første opkald til operatør nyt 

Lazy class-indlæsning er et vigtigt træk ved Java-runtime-miljøet, da det under visse omstændigheder kan reducere hukommelsesforbruget. For eksempel, hvis en del af et program aldrig udføres under en session, vil klasser, der kun henvises til i den del af programmet, aldrig blive indlæst.

Lazy object creation

Oprettelse af dovne objekter er tæt forbundet med indlæsning af doven klasse. Første gang du bruger det nye søgeord på en klassetype, der tidligere ikke er blevet indlæst, indlæses Java-runtime for dig. Oprettelse af dovne objekter kan reducere hukommelsesforbruget i meget større grad end doven klasseindlæsning.

For at introducere begrebet skabelse af doven objekt, lad os se på et simpelt kodeeksempel, hvor en Ramme bruger en MessageBox for at få vist fejlmeddelelser:

offentlig klasse MyFrame udvider ramme {privat MessageBox mb_ = ny MessageBox (); // privat hjælper, der bruges af denne klasse private ugyldige showMessage (streng besked) {// indstil beskedteksten mb_.setMessage (besked); mb_.pack (); mb_.show (); }} 

I ovenstående eksempel, når en forekomst af MyFrame er oprettet, MessageBox forekomst mb_ oprettes også. De samme regler gælder rekursivt. Så enhver instansvariabel initialiseret eller tildelt i klassen MessageBoxKonstruktør er også tildelt fra bunken og så videre. Hvis forekomsten af MyFrame bruges ikke til at vise en fejlmeddelelse i en session, vi spilder hukommelsen unødigt.

I dette ret enkle eksempel vil vi ikke virkelig vinde for meget. Men hvis du overvejer en mere kompleks klasse, der bruger mange andre klasser, som igen bruger og instantierer flere objekter rekursivt, er det potentielle hukommelsesforbrug mere tydeligt.

Overvej doven instantiering som en politik til at reducere ressourcebehov

Den dovne tilgang til ovenstående eksempel er angivet nedenfor, hvor objekt mb_ er instanseret ved det første opkald til showMessage (). (Det vil sige ikke før det faktisk er nødvendigt af programmet.)

offentlig endelig klasse MyFrame udvider ramme {privat MessageBox mb_; // null, implicit // privat hjælper, der bruges af denne klasse private ugyldige showMessage (streng besked) {if (mb _ == null) // første kald til denne metode mb_ = ny MessageBox (); // indstil beskedteksten mb_.setMessage (besked); mb_.pack (); mb_.show (); }} 

Hvis du ser nærmere på showMessage (), vil du se, at vi først bestemmer, om forekomstvariablen mb_ er lig med null. Da vi ikke har initialiseret mb_ på sit erklæringssted, har Java-runtime taget sig af dette for os. Således kan vi trygt fortsætte ved at oprette MessageBox eksempel. Alle fremtidige opkald til showMessage () finder ud af, at mb_ ikke er lig med null, og derfor springer oprettelsen af ​​objektet over og bruger den eksisterende forekomst.

Et virkeligt eksempel

Lad os nu undersøge et mere realistisk eksempel, hvor doven instantiering kan spille en nøglerolle i at reducere mængden af ​​ressourcer, der bruges af et program.

Antag, at vi er bedt af en klient om at skrive et system, der giver brugerne mulighed for at katalogisere billeder på et filsystem og give mulighed for at se enten miniaturer eller komplette billeder. Vores første forsøg kan være at skrive en klasse, der indlæser billedet i sin konstruktør.

offentlig klasse ImageFile {privat streng filnavn_; privat billede image_; offentlig ImageFile (strengfilnavn) {filnavn_ = filnavn; // indlæse billedet} public String getName () {return filnavn_;} public Image getImage () {return image_; }} 

I eksemplet ovenfor, ImageFile implementerer en alt for stor tilgang til at instantere Billede objekt. Til sin fordel garanterer dette design, at et billede vil være tilgængeligt straks på tidspunktet for et opkald til getImage (). Dette kunne dog ikke kun være smertefuldt langsomt (i tilfælde af et bibliotek, der indeholder mange billeder), men dette design kunne udtømme den tilgængelige hukommelse. For at undgå disse potentielle problemer kan vi bytte ydelsesfordelene ved øjeblikkelig adgang til reduceret hukommelsesforbrug. Som du måske har gættet, kan vi opnå dette ved at bruge doven instantiering.

Her er den opdaterede ImageFile klasse ved hjælp af samme tilgang som klasse MyFrame gjorde med sin MessageBox instansvariabel:

offentlig klasse ImageFile {privat streng filnavn_; privat billede image_; // = null, implicit offentlig ImageFile (strengfilnavn) {// gemmer kun filnavnet filnavn_ = filnavn; } public String getName () {return filnavn_;} public Image getImage () {if (image _ == null) {// første kald til getImage () // indlæs billedet ...} returner image_; }} 

I denne version indlæses det faktiske billede kun ved det første opkald til getImage (). Så for at opsummere er kompromisen her, at for at reducere den samlede hukommelsesforbrug og opstartstider, betaler vi prisen for at indlæse billedet første gang, det bliver anmodet om - at introducere et præstationshit på det tidspunkt i programmets udførelse. Dette er et andet udtryk, der afspejler Proxy mønster i en sammenhæng, der kræver en begrænset brug af hukommelse.

Politikken for doven instantiering illustreret ovenfor er fint for vores eksempler, men senere vil du se, hvordan designet skal ændre sig i sammenhæng med flere tråde.

Lazy instantiering for Singleton-mønstre i Java

Lad os nu se på Singleton-mønsteret. Her er den generiske form i Java:

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

I den generiske version erklærede og initialiserede vi forekomst_ felt som følger:

statisk endelig Singleton-forekomst_ = ny Singleton (); 

Læsere, der er fortrolige med C ++ implementeringen af ​​Singleton skrevet af GoF (Gang of Four, der skrev bogen Designmønstre: Elementer af genanvendelig objektorienteret software - Gamma, Helm, Johnson og Vlissides) kan blive overrasket over, at vi ikke udsætter initialiseringen af forekomst_ felt indtil opkaldet til forekomst () metode. Således bruger man doven instantiering:

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

Ovenstående liste er en direkte port af C ++ Singleton-eksemplet givet af GoF og ofte udråbt som den generiske Java-version også. Hvis du allerede er bekendt med denne formular og var overrasket over, at vi ikke listede vores generiske Singleton sådan, vil du blive endnu mere overrasket over at høre, at det er helt unødvendigt i Java! Dette er et almindeligt eksempel på, hvad der kan opstå, hvis du porterer kode fra et sprog til et andet uden at overveje de respektive runtime-miljøer.

For ordens skyld bruger GoFs C ++ - version af Singleton doven instantiering, fordi der ikke er nogen garanti for rækkefølgen af ​​statisk initialisering af objekter ved kørsel. (Se Scott Meyers Singleton for en alternativ tilgang i C ++.) I Java behøver vi ikke bekymre os om disse problemer.

Den dovne tilgang til at starte en Singleton er unødvendig i Java på grund af den måde, hvorpå Java-runtime håndterer klasseindlæsning og statisk forekomst af variabel initialisering. Tidligere har vi beskrevet, hvordan og hvornår klasser bliver indlæst. En klasse med kun offentlige statiske metoder indlæses af Java-runtime ved det første opkald til en af ​​disse metoder; hvilket i tilfældet med vores Singleton er

Singleton s = Singleton.instance (); 

Det første opkald til Singleton.instance () i et program tvinger Java-runtime til at indlæse klassen Singleton. Som marken forekomst_ erklæres som statisk, vil Java-runtime initialisere den efter indlæsning af klassen. Således garanterer, at opkaldet til Singleton.instance () vil returnere en fuldt initialiseret Singleton - få billedet?

Lazy instantiering: farlig i flertrådede applikationer

Brug af doven instantiering til et konkret Singleton er ikke kun unødvendigt i Java, det er ligefrem farligt i forbindelse med multitrådede applikationer. Overvej den dovne version af Singleton.instance () metode, hvor to eller flere separate tråde forsøger at opnå en reference til objektet via forekomst (). Hvis en tråd er forudbestemt efter at linjen er udført hvis (forekomst _ == null), men før den har afsluttet linjen forekomst_ = ny Singleton (), kan en anden tråd også gå ind i denne metode med forekomst_ stadig == null - grimt!

Resultatet af dette scenario er sandsynligheden for, at der oprettes et eller flere Singleton-objekter. Dette er en stor hovedpine, når din Singleton-klasse f.eks. Opretter forbindelse til en database eller fjernserver. Den enkle løsning på dette problem ville være at bruge det synkroniserede nøgleord til at beskytte metoden mod flere tråde, der kommer ind på samme tid:

synkroniseret statisk offentlig instans () {...} 

Denne tilgang er dog lidt hårdhændet for de fleste multitrådede applikationer, der bruger en Singleton-klasse i vid udstrækning og derved forårsager blokering af samtidige opkald til forekomst (). Forresten er det altid meget langsommere at påberåbe sig en synkroniseret metode end at påberåbe sig en ikke-synkroniseret metode. Så hvad vi har brug for er en strategi for synkronisering, der ikke forårsager unødvendig blokering. Heldigvis findes en sådan strategi. Det er kendt som dobbeltklik idiom.

Dobbeltcheck-idiomet

Brug ordet med dobbeltkontrol for at beskytte metoder, der bruger doven instantiering. Sådan implementeres det i Java:

offentlig statisk Singleton-forekomst () {hvis (forekomst _ == null) // ikke ønsker at blokere her {// to eller flere tråde kan være her !!! synkroniseret (Singleton.class) {// skal kontrollere igen, da en af ​​de // blokerede tråde stadig kan indtaste, hvis (instans _ == null) forekomst_ = ny Singleton (); // sikker}} returnerer forekomst_; } 

Dobbeltkontrol-idiomet forbedrer ydeevnen ved kun at bruge synkronisering, hvis flere tråde kalder forekomst () inden Singleton konstrueres. Når objektet er instantieret, forekomst_ er ikke længere == null, der gør det muligt for metoden at undgå at blokere samtidige opkald.

Brug af flere tråde i Java kan være meget kompleks. Faktisk er emnet samtidighed så stort, at Doug Lea har skrevet en hel bog om det: Samtidig programmering i Java. Hvis du er ny med samtidig programmering, anbefaler vi, at du får en kopi af denne bog, inden du går i gang med at skrive komplekse Java-systemer, der er afhængige af flere tråde.