Programmering

Undgå blokering af synkronisering

I min tidligere artikel "Double-Checked Locking: Clever, but Broken" (JavaWorld, Februar 2001) beskrev jeg, hvordan flere almindelige teknikker til at undgå synkronisering faktisk er usikre, og anbefalede en strategi med "Når du er i tvivl, synkroniser." Generelt skal du synkronisere, når du læser en variabel, der muligvis tidligere er skrevet af en anden tråd, eller når du skriver en variabel, der efterfølgende kan læses af en anden tråd. Derudover, mens synkronisering medfører en præstationsstraff, er straffen forbundet med utilsigtet synkronisering ikke så stor som nogle kilder har antydet, og er reduceret støt med hver efterfølgende JVM-implementering. Så det ser ud til, at der nu er mindre grund end nogensinde til at undgå synkronisering. En anden risiko er imidlertid forbundet med overdreven synkronisering: blokering.

Hvad er en dødvande?

Vi siger, at et sæt processer eller tråde er fastlåst når hver tråd venter på en begivenhed, som kun en anden proces i sættet kan forårsage. En anden måde at illustrere en blokering på er at oprette en rettet graf, hvis hjørner er tråde eller processer, og hvis kanter repræsenterer "venter på" -forholdet. Hvis denne graf indeholder en cyklus, er systemet blokeret. Medmindre systemet er designet til at komme sig fra blokeringer, får en blokering programmet eller systemet til at hænge.

Synkronisering af blokeringer i Java-programmer

Dødlåse kan forekomme i Java, fordi synkroniseret nøgleord får den udførende tråd til at blokere, mens de venter på låsen eller skærmen, der er knyttet til det angivne objekt. Da tråden måske allerede indeholder låse, der er knyttet til andre objekter, kunne to tråde hver vente på, at den anden frigør en lås; i et sådant tilfælde vil de ende med at vente for evigt. Følgende eksempel viser et sæt metoder, der har potentiale til blokering. Begge metoder erhverver låse på to låseobjekter, cacheLock og bordlås, inden de fortsætter. I dette eksempel er objekterne, der fungerer som låse, globale (statiske) variabler, en almindelig teknik til forenkling af applikationslåsningsadfærd ved at udføre låsning på et grovere granularitetsniveau:

Notering 1. En potentiel synkroniseringsdødlås

 offentlig statisk objekt cacheLock = nyt objekt (); offentlig statisk Object tableLock = nyt objekt (); ... offentlig ugyldighed oneMethod () {synkroniseret (cacheLock) {synkroniseret (tableLock) {doSomething (); }}} offentlig annullerer anotherMethod () {synkroniseret (tableLock) {synkroniseret (cacheLock) {doSomethingElse (); }}} 

Forestil dig nu, at tråd A kalder oneMethod () mens tråd B samtidigt kalder anotherMethod (). Forestil dig yderligere, at tråd A får låsen på cacheLock, og på samme tid får tråd B låsen på bordlås. Nu er trådene låst fast: ingen af ​​trådene giver afstand til låsen, indtil den får den anden lås, men ingen vil være i stand til at erhverve den anden lås, før den anden tråd giver den op. Når et Java-program blokerer, venter de fastlåste tråde simpelthen for evigt. Mens andre tråde muligvis fortsætter med at køre, bliver du til sidst nødt til at dræbe programmet, genstarte det og håbe, at det ikke blokerer igen.

Test af deadlocks er vanskelig, da deadlocks afhænger af timing, belastning og miljø og dermed kan ske sjældent eller kun under visse omstændigheder. Kode kan have potentiale for blokering, som Listing 1, men udviser ikke blokering, før der forekommer en kombination af tilfældige og ikke tilfældige begivenheder, såsom at programmet udsættes for et bestemt belastningsniveau, kører på en bestemt hardwarekonfiguration eller udsættes for en bestemt blanding af brugerhandlinger og miljøforhold. Deadlocks ligner tidsbomber, der venter på at eksplodere i vores kode; når de gør det, hænger vores programmer simpelthen.

Inkonsekvent låsebestilling forårsager blokeringer

Heldigvis kan vi stille et relativt simpelt krav til låseanskaffelse, der kan forhindre synkronisering af blokeringer. Listing 1's metoder har potentiale for blokering, fordi hver metode erhverver de to låse i en anden rækkefølge. Hvis Listing 1 var skrevet således, at hver metode erhvervede de to låse i samme rækkefølge, kunne to eller flere tråde, der udførte disse metoder, ikke blokere, uanset timing eller andre eksterne faktorer, fordi ingen tråd kunne erhverve den anden lås uden allerede at holde først. Hvis du kan garantere, at låse altid erhverves i en ensartet rækkefølge, vil dit program ikke låse fast.

Dødlåse er ikke altid så indlysende

Når du er tilpasset vigtigheden af ​​låsebestilling, kan du nemt genkende Listing 1's problem. Imidlertid kan analoge problemer vise sig mindre åbenlyse: måske findes de to metoder i separate klasser, eller måske erhverves de involverede låse implicit ved at ringe til synkroniserede metoder i stedet for eksplicit via en synkroniseret blok. Overvej disse to samarbejdende klasser, Model og Udsigti en forenklet MVC-ramme (Model-View-Controller):

Notering 2. En mere subtil potentiel synkroniseringsdødlås

 offentlig klassemodel {privat Vis myView; offentlig synkroniseret ugyldig opdateringsmodel (Objekt someArg) {doSomething (someArg); myView.somethingChanged (); } offentligt synkroniseret objekt getSomething () {returner someMethod (); }} offentlig klasse Vis {privat model underliggende model; offentlig synkroniseret ugyldig nogetChanged () {doSomething (); } offentlig synkroniseret ugyldig opdateringsvisning () {Objekt o = myModel.getSomething (); }} 

Liste 2 har to samarbejdende objekter, der har synkroniserede metoder; hvert objekt kalder den andres synkroniserede metoder. Denne situation ligner liste 1 - to metoder skaffer låse på de samme to objekter, men i forskellige ordrer. Imidlertid er den inkonsekvente låsebestilling i dette eksempel meget mindre åbenlyst end i Listing 1, fordi låseanskaffelsen er en implicit del af metodekaldet. Hvis en tråd ringer Model.updateModel () mens en anden tråd samtidigt ringer View.updateView (), kunne den første tråd opnå den Modellås og vent på Udsigtlås, mens den anden opnår Udsigt's lås og venter for evigt på Modellås.

Du kan begrave potentialet for synkroniseringsblok endnu dybere. Overvej dette eksempel: Du har en metode til at overføre penge fra en konto til en anden. Du ønsker at erhverve låse på begge konti, før du udfører overførslen for at sikre, at overførslen er atomær. Overvej denne harmløse implementering:

Listing 3. En endnu mere subtil potentiel synkroniseringsdødlås

 offentlig ugyldig overførselMoney (konto fra konto, konto til konto, dollarbeløbbeløbTil overførsel) {synkroniseret (fraAccount) {synkroniseret (tilAccount) {hvis (fraAccount.hasSufficientBalance (beløbToTransfer) {fromAccount.debit (beløbToTransfer); toAccount.credit (beløb)} (beløb) } 

Selvom alle metoder, der fungerer på to eller flere konti, bruger den samme rækkefølge, indeholder Listing 3 frøene til det samme blokeringsproblem som Listing 1 og 2, men på en endnu mere subtil måde. Overvej hvad der sker, når tråd A udfører:

 transferMoney (accountOne, accountTwo, beløb); 

Mens tråd B på samme tid udfører:

 transferMoney (accountTwo, accountOne, anotherAmount); 

Igen forsøger de to tråde at erhverve de samme to låse, men i forskellige ordrer; dødvandsrisikoen vævede stadig, men i en meget mindre indlysende form.

Sådan undgår du blokeringer

En af de bedste måder at forhindre potentialet for blokering er at undgå at erhverve mere end en lås ad gangen, hvilket ofte er praktisk. Men hvis det ikke er muligt, har du brug for en strategi, der sikrer, at du erhverver flere låse i en ensartet, defineret rækkefølge.

Afhængigt af hvordan dit program bruger låse, er det muligvis ikke kompliceret at sikre, at du bruger en ensartet låserækkefølge. I nogle programmer, f.eks. I liste 1, trækkes alle kritiske låse, der kan deltage i flere låse, fra et lille sæt singleton-låseobjekter. I så fald kan du definere en ordre på låseanskaffelse på sæt låse og sikre, at du altid erhverver låse i den rækkefølge. Når låseordren er defineret, skal den simpelthen være veldokumenteret for at tilskynde til ensartet brug i hele programmet.

Krymp synkroniserede blokke for at undgå flere låse

I lister 2 vokser problemet mere kompliceret, fordi låsene erhverves implicit som et resultat af at kalde en synkroniseret metode. Du kan normalt undgå den slags potentielle deadlocks, der opstår fra tilfælde som Listing 2, ved at indsnævre synkroniseringens omfang til en så lille blok som muligt. Gør det Model.updateModel () virkelig har brug for at holde Model låse, mens den ringer View.somethingChanged ()? Ofte gør det ikke; hele metoden blev sandsynligvis synkroniseret som en genvej snarere end fordi hele metoden skulle synkroniseres. Men hvis du udskifter synkroniserede metoder med mindre synkroniserede blokke inde i metoden, skal du dokumentere denne låseadfærd som en del af metodens Javadoc. Opkaldere skal vide, at de kan ringe til metoden sikkert uden ekstern synkronisering. Opkaldere skal også kende metodens låseadfærd, så de kan sikre, at låse erhverves i en ensartet rækkefølge.

En mere sofistikeret låsebestillingsteknik

I andre situationer, som f.eks. Listing 3's bankkontoeksempel, bliver anvendelsen af ​​reglen med fast ordre endnu mere kompliceret; du er nødt til at definere en samlet ordre på det sæt objekter, der er berettiget til at låse, og brug denne rækkefølge til at vælge rækkefølgen af ​​låsoptagelse. Dette lyder rodet, men er faktisk ligetil. Liste 4 illustrerer denne teknik; det bruger et numerisk kontonummer til at fremkalde en ordre Konto genstande. (Hvis det objekt, du skal låse, mangler en naturlig identitetsegenskab som et kontonummer, kan du bruge Object.identityHashCode () metode til at generere en i stedet.)

Fortegnelse 4. Brug en bestilling til at erhverve låse i en fast rækkefølge

 offentlig ugyldig transferMoney (konto fra konto, konto til konto, dollarbeløb beløb til overførsel) {konto firstLock, secondLock; if (fromAccount.accountNumber () == toAccount.accountNumber ()) throw new Exception ("Kan ikke overføre fra konto til sig selv"); ellers hvis (fromAccount.accountNumber () <toAccount.accountNumber ()) {firstLock = fromAccount; secondLock = toAccount; } andet {firstLock = toAccount; secondLock = fromAccount; } synkroniseret (firstLock) {synchronised (secondLock) {if (fromAccount.hasSufficientBalance (amountToTransfer) {fromAccount.debit (amountToTransfer); toAccount.credit (amountToTransfer);}}}} 

Nu den rækkefølge, som konti er specificeret i opkaldet til overføre penge() betyder ikke noget; låse anskaffes altid i samme rækkefølge.

Den vigtigste del: Dokumentation

Et kritisk - men ofte overset - element i enhver låsestrategi er dokumentation. Desværre, selv i tilfælde, hvor der lægges stor vægt på at designe en låsestrategi, bruges der ofte meget mindre kræfter på at dokumentere det. Hvis dit program bruger et lille sæt singleton-låse, bør du dokumentere dine antagelser om låsebestilling så klart som muligt, så fremtidige vedligeholdere kan opfylde kravene til låsebestilling. Hvis en metode skal erhverve en lås for at udføre sin funktion eller skal kaldes med en bestemt lås, skal metodens Javadoc bemærke det faktum. På den måde vil fremtidige udviklere vide, at opkald til en given metode kan medføre erhvervelse af en lås.

Få programmer eller klassebiblioteker dokumenterer tilstrækkeligt deres låseanvendelse. I det mindste skal hver metode dokumentere de låse, den erhverver, og om opkaldere skal holde en lås for at ringe til metoden sikkert. Derudover skal klasser dokumentere, om de er trådsikre eller ej, eller under hvilke betingelser.

Fokus på låseadfærd på designtidspunktet

Fordi blokeringer ofte ikke er indlysende og forekommer sjældent og uforudsigeligt, kan de forårsage alvorlige problemer i Java-programmer. Ved at være opmærksom på dit programs låseadfærd på designtidspunktet og definere regler for, hvornår og hvordan du får flere låse, kan du reducere sandsynligheden for blokeringer betydeligt. Husk at dokumentere dit programs låseanskaffelsesregler og dets brug af synkronisering omhyggeligt; den tid, der bruges til at dokumentere enkle låseforudsætninger, vil betale sig ved kraftigt at reducere risikoen for blokering og andre samtidige problemer senere.

Brian Goetz er en professionel softwareudvikler med mere end 15 års erfaring. Han er hovedkonsulent hos Quiotix, et softwareudviklings- og konsulentfirma i Los Altos, Californien.