Programmering

Unngå blokkeringer for synkronisering

I min tidligere artikkel "Double-Checked Locking: Clever, but Broken" (JavaWorld, Februar 2001) beskrev jeg hvordan flere vanlige teknikker for å unngå synkronisering faktisk er usikre, og anbefalte strategien "Når du er i tvil, synkroniser." Generelt sett bør du synkronisere når du leser en variabel som tidligere har blitt skrevet av en annen tråd, eller når du skriver en variabel som senere kan bli lest av en annen tråd. I tillegg, mens synkronisering medfører en ytelsesstraff, er straffen forbundet med ukontrollert synkronisering ikke så stor som noen kilder har antydet, og har redusert jevnt med hver påfølgende JVM-implementering. Så det ser ut til at det nå er mindre grunn enn noensinne til å unngå synkronisering. Imidlertid er en annen risiko forbundet med overdreven synkronisering: fastlåst.

Hva er en fastlåst tilstand?

Vi sier at et sett med prosesser eller tråder er fastlåst når hver tråd venter på en hendelse som bare en annen prosess i settet kan forårsake. En annen måte å illustrere en fastlåst tilstand på er å bygge en rettet graf hvis hjørner er tråder eller prosesser og hvis kanter representerer forholdet "venter på". Hvis denne grafen inneholder en syklus, er systemet låst. Med mindre systemet er designet for å gjenopprette fra fastlåsning, får en fastlåsning programmet eller systemet til å henge.

Synkroniseringslåser i Java-programmer

Blokkeringer kan forekomme i Java fordi synkronisert nøkkelord får den utførende tråden til å blokkere mens du venter på låsen, eller skjermen, assosiert med det spesifiserte objektet. Siden tråden allerede kan inneholde låser knyttet til andre gjenstander, kan to tråder vente på at den andre skal frigjøre en lås; i et slikt tilfelle vil de ende opp med å vente for alltid. Følgende eksempel viser et sett med metoder som har potensial for fastlåsning. Begge metodene skaffer seg låser på to låseobjekter, cacheLock og tableLock, før de fortsetter. I dette eksemplet er objektene som fungerer som låser globale (statiske) variabler, en vanlig teknikk for å forenkle applikasjonslåsende atferd ved å utføre låsning på et grovere granularitetsnivå:

Oppføring 1. En potensiell synkroniseringslås

 offentlig statisk objekt cacheLock = nytt objekt (); offentlig statisk Objekt tableLock = nytt Objekt (); ... offentlig ugyldig oneMethod () {synkronisert (cacheLock) {synkronisert (tableLock) {doSomething (); }}} offentlig annullere anotherMethod () {synkronisert (tableLock) {synkronisert (cacheLock) {doSomethingElse (); }}} 

Tenk deg nå at tråden A ringer oneMethod () mens tråd B samtidig ringer anotherMethod (). Tenk deg videre at tråd A får låsen på cacheLock, og på samme tid får tråd B låsen på tableLock. Nå er trådene låst fast: ingen av trådene gir opp låsen før den får den andre låsen, men ingen av dem vil være i stand til å anskaffe den andre låsen før den andre tråden gir den opp. Når et Java-program blokkerer, venter de fastlåste trådene for alltid. Mens andre tråder kan fortsette å kjøre, må du til slutt drepe programmet, starte det på nytt, og håpe at det ikke går igjen.

Det er vanskelig å teste for fastlåsning, ettersom fastlåsning er avhengig av tidspunkt, belastning og miljø, og dermed kan skje sjeldent eller bare under visse omstendigheter. Kode kan ha potensial for låsing, som oppføring 1, men ikke vise lås før noen kombinasjon av tilfeldige og ikke-tilfeldige hendelser oppstår, for eksempel at programmet blir utsatt for et visst belastningsnivå, kjøres på en bestemt maskinvarekonfigurasjon eller utsettes for en viss blanding av brukerhandlinger og miljøforhold. Låseblokker ligner tidsbomber som venter på å eksplodere i koden vår; når de gjør det, henger programmene rett og slett.

Inkonsekvent låsebestilling forårsaker fastlåsning

Heldigvis kan vi stille et relativt enkelt krav til låseanskaffelse som kan forhindre låsing av synkronisering. Listing 1s metoder har potensial for låsing fordi hver metode skaffer seg de to låsene i en annen rekkefølge. Hvis oppføring 1 hadde blitt skrevet slik at hver metode tilegnet seg de to låser i samme rekkefølge, kunne to eller flere tråder som utførte disse metodene ikke låse fast, uavhengig av timing eller andre eksterne faktorer, fordi ingen tråder kunne anskaffe den andre låsen uten å allerede ha holdt først. Hvis du kan garantere at låser alltid blir anskaffet i en jevn rekkefølge, vil ikke programmet låse fast.

Låsebrikker er ikke alltid så åpenbare

Når du er tilpasset viktigheten av låsebestilling, kan du enkelt gjenkjenne Listing 1s problem. Imidlertid kan analoge problemer vise seg å være mindre åpenbare: kanskje ligger de to metodene i separate klasser, eller kanskje er de involverte låsene ervervet implisitt ved å ringe synkroniserte metoder i stedet for eksplisitt via en synkronisert blokk. Tenk på disse to samarbeidende klassene, Modell og Utsikt, i et forenklet MVC-rammeverk (Model-View-Controller):

Oppføring 2. En mer subtil potensiell synkroniseringslås

 public class Model {private View myView; offentlig synkronisert ugyldig oppdateringsmodell (Objekt someArg) {doSomething (someArg); myView.somethingChanged (); } offentlig synkronisert objekt getSomething () {return someMethod (); }} offentlig klasse Vis {privat modell underliggende modell; offentlig synkronisert ugyldig noeChanged () {doSomething (); } offentlig synkronisert ugyldig updateView () {Object o = myModel.getSomething (); }} 

Oppføring 2 har to samarbeidende objekter som har synkroniserte metoder; hvert objekt kaller den andres synkroniserte metoder. Denne situasjonen ligner oppføring 1 - to metoder skaffer seg låser på de samme to objektene, men i forskjellige rekkefølge. Imidlertid er den inkonsekvente låsrekkefølgen i dette eksemplet mye mindre åpenbar enn den i liste 1 fordi låseanskaffelsen er en implisitt del av metodeanropet. Hvis en tråd ringer Model.updateModel () mens en annen tråd ringer samtidig View.updateView (), kunne den første tråden oppnå Modelllås og vent på Utsiktlåsen, mens den andre får tak i UtsiktLås og venter for alltid på Modelllås.

Du kan begrave potensialet for synkroniseringslås enda dypere. Tenk på dette eksemplet: Du har en metode for å overføre penger fra en konto til en annen. Du vil anskaffe låser på begge kontoene før du utfører overføringen for å sikre at overføringen er atomær. Vurder denne harmløse implementeringen:

Listing 3. En enda mer subtil potensiell synkroniseringslås

 offentlig ugyldig overføringMoney (konto fra konto, konto til konto, dollarbeløp beløp til overføring) {synkronisert (fra konto) {synkronisert (til konto) {hvis (fra konto.hassufficientbalance (beløpToTransfer) {fromAccount.debit (beløpToTransfer); toAccount.credit (beløp)} (beløp) } 

Selv om alle metoder som opererer på to eller flere kontoer bruker samme bestilling, inneholder Listing 3 frøene til det samme fastlåste problemet som Listing 1 og 2, men på en enda subtilere måte. Tenk på hva som skjer når tråd A kjører:

 transferMoney (accountOne, accountTwo, beløp); 

Samtidig utfører tråd B:

 transferMoney (accountTwo, accountOne, anotherAmount); 

Igjen prøver de to trådene å skaffe seg de samme to låser, men i forskjellige rekkefølge; fastlåst risiko risikerer fortsatt, men i en mye mindre åpenbar form.

Hvordan unngå fastlåsning

En av de beste måtene å forhindre potensialet for fastlåsning er å unngå å anskaffe mer enn en lås om gangen, noe som ofte er praktisk. Imidlertid, hvis det ikke er mulig, trenger du en strategi som sikrer at du anskaffer flere låser i en jevn, definert rekkefølge.

Avhengig av hvordan programmet ditt bruker låser, kan det hende det ikke er komplisert å sikre at du bruker en jevn låsingsrekkefølge. I noen programmer, for eksempel i liste 1, blir alle kritiske låser som kan delta i flere låser hentet fra et lite sett med enkeltlåsobjekter. I så fall kan du definere en ordre for låseanskaffelse på settet med låser og sikre at du alltid anskaffer låser i den rekkefølgen. Når låsrekkefølgen er definert, må den ganske enkelt være godt dokumentert for å oppmuntre til konsistent bruk gjennom hele programmet.

Krymp synkroniserte blokker for å unngå flere låser

I oppføring 2 blir problemet mer komplisert fordi låsene ervervet implisitt som et resultat av å kalle en synkronisert metode. Du kan vanligvis unngå den slags potensielle fastlåser som følger av tilfeller som Listing 2 ved å begrense synkroniseringsomfanget til en så liten blokk som mulig. Gjør Model.updateModel () virkelig trenger å holde Modell låse mens den ringer View.somethingChanged ()? Ofte gjør det ikke; hele metoden ble sannsynligvis synkronisert som en snarvei, snarere enn fordi hele metoden måtte synkroniseres. Imidlertid, hvis du erstatter synkroniserte metoder med mindre synkroniserte blokker inne i metoden, må du dokumentere denne låseatferden som en del av metodens Javadoc. Innringere må vite at de kan ringe metoden trygt uten ekstern synkronisering. Innringere bør også kjenne metodens låseatferd, slik at de kan sikre at låser anskaffes i en konsekvent rekkefølge.

En mer sofistikert teknikk for låsebestilling

I andre situasjoner, som for eksempel Listing 3s bankkontoeksempel, blir det enda mer komplisert å bruke fastbestillingsregelen; du må definere en totalbestilling på settet med objekter som er kvalifisert for låsing, og bruk denne bestillingen til å velge sekvensen for låseanskaffelse. Dette høres rotete ut, men er faktisk greit. Oppføring 4 illustrerer den teknikken; den bruker et numerisk kontonummer for å indusere en bestilling Regnskap gjenstander. (Hvis objektet du trenger å låse mangler en naturlig identitetsegenskap som et kontonummer, kan du bruke Object.identityHashCode () metode for å generere en i stedet.)

Oppføring 4. Bruk en bestilling for å anskaffe låser i en fast rekkefølge

 public void transferMoney (Account fromAccount, Account toAccount, DollarAmount amountToTransfer) {Konto firstLock, secondLock; if (fromAccount.accountNumber () == toAccount.accountNumber ()) throw new Exception ("Kan ikke overføre fra konto til seg selv"); annet hvis (fromAccount.accountNumber () <toAccount.accountNumber ()) {firstLock = fromAccount; secondLock = toAccount; } annet {firstLock = toAccount; secondLock = fromAccount; } synkronisert (firstLock) {synchronized (secondLock) {if (fromAccount.hasSufficientBalance (amountToTransfer) {fromAccount.debit (amountToTransfer); toAccount.credit (amountToTransfer);}}}} 

Nå rekkefølgen som kontoene er spesifisert i samtalen til overføre penger() spiller ingen rolle; låsene anskaffes alltid i samme rekkefølge.

Den viktigste delen: Dokumentasjon

Et kritisk - men ofte oversett - element i enhver låsestrategi er dokumentasjon. Dessverre, selv i tilfeller der det er lagt stor vekt på å utforme en låsestrategi, blir ofte mye mindre innsats brukt på å dokumentere den. Hvis programmet ditt bruker et lite sett med enkeltlåser, bør du dokumentere antagelsene om låsbestilling så tydelig som mulig, slik at fremtidige vedlikeholdere kan oppfylle kravene til låsbestilling. Hvis en metode må anskaffe en lås for å utføre sin funksjon, eller må ringes med en bestemt lås holdt, bør metodens Javadoc merke seg det faktum. På den måten vil fremtidige utviklere vite at å ringe til en gitt metode kan innebære å anskaffe en lås.

Få programmer eller klassebiblioteker dokumenterer tilstrekkelig låsebruk. I det minste bør hver metode dokumentere låsene den anskaffer, og om innringere må holde en lås for å ringe metoden trygt. I tillegg bør klasser dokumentere om de er trådsikre eller ikke, eller under hvilke forhold.

Fokuser på låseadferd ved designtid

Fordi fastlåsning ofte ikke er åpenbar og forekommer sjelden og uforutsigbart, kan de forårsake alvorlige problemer i Java-programmer. Ved å være oppmerksom på programmets låseadferd på designtid og definere regler for når og hvordan du skaffer deg flere låser, kan du redusere sannsynligheten for låsing betydelig. Husk å dokumentere programmets låseanskaffelsesregler og dets bruk av synkronisering nøye; tiden brukt på å dokumentere enkle låseforutsetninger vil lønne seg ved å redusere sjansen for lås og andre samtidige problemer senere.

Brian Goetz er en profesjonell programvareutvikler med mer enn 15 års erfaring. Han er hovedkonsulent i Quiotix, et programvareutviklings- og konsulentfirma i Los Altos, California.
$config[zx-auto] not found$config[zx-overlay] not found