Programiranje

Izogibajte se zastojem pri sinhronizaciji

V mojem prejšnjem članku "Dvakrat preverjeno zaklepanje: pametno, a pokvarjeno" (JavaWorld, Februarja 2001), opisal sem, kako je nekaj običajnih tehnik izogibanja sinhronizaciji v resnici nevarnih, in priporočil strategijo "Kadar dvomite, sinhronizirajte." Na splošno bi morali sinhronizirati, kadar berete katero koli spremenljivko, ki jo je prej napisala druga nit, ali kadar pišete katero koli spremenljivko, ki bi jo lahko nato prebrala druga nit. Poleg tega, medtem ko sinhronizacija zahteva kazen za zmogljivost, kazen, povezana z neomejeno sinhronizacijo, ni tako velika, kot predlagajo nekateri viri, in se je z vsako zaporedno izvedbo JVM enakomerno zmanjševala. Zdi se, da je zdaj razloga za izogibanje sinhronizaciji manj kot kdaj koli prej. Vendar pa je s pretirano sinhronizacijo povezano še eno tveganje: zastoj.

Kaj je zastoj?

Pravimo, da je niz procesov ali niti zastoj ko vsaka nit čaka na dogodek, ki ga lahko povzroči samo še en postopek v nizu. Drug način za ponazoritev zastoja je izdelava usmerjenega grafa, katerega oglišča so niti ali procesi in katerih robovi predstavljajo razmerje "čakanje čakamo". Če ta graf vsebuje cikel, je sistem v blokadi. Če sistem ni zasnovan tako, da se obnovi z mrtve točke, program ali sistem obesi zaradi blokade.

Zastoji pri sinhronizaciji v programih Java

V Javi se lahko pojavijo mrtve točke, ker sinhronizirano ključna beseda povzroči, da se izvršilna nit blokira, medtem ko čaka na zaklepanje ali monitor, povezan z navedenim predmetom. Ker lahko nit že vsebuje ključavnice, povezane z drugimi predmeti, lahko dve niti čakata, da druga sprosti ključavnico; v takem primeru bodo na koncu čakali večno. Naslednji primer prikazuje nabor metod, ki lahko povzročijo blokado. Obe metodi pridobita ključavnice na dveh objektih zaklepanja, cacheLock in tableLock, preden nadaljujejo. V tem primeru so predmeti, ki delujejo kot ključavnice, globalne (statične) spremenljivke, običajna tehnika za poenostavitev vedenja zaklepanja aplikacij z izvedbo zaklepanja na grobši ravni razdrobljenosti:

Seznam 1. Potencialna zastoj sinhronizacije

 javni statični objekt cacheLock = nov objekt (); javni statični objekt tableLock = nov objekt (); ... javna void oneMethod () {sinhronizirano (cacheLock) {sinhronizirano (tableLock) {doSomething (); }}} javna void anotherMethod () {sinhronizirano (tableLock) {sinhronizirano (cacheLock) {doSomethingElse (); }}} 

Zdaj pa si predstavljajte, da kliče nit A oneMethod () medtem ko nit B istočasno kliče anotherMethod (). Nadalje si predstavljajte, da nit A pridobi ključavnico naprej cacheLockin hkrati nit B pridobi ključavnico tableLock. Zdaj so niti v blokadi: nobena nit se ne bo odrekla ključavnici, dokler ne pridobi druge ključavnice, druga pa ne bo mogla pridobiti druge ključavnice, dokler je druga nit ne odstopi. Ko se program Java zablokira, niti, ki blokirajo, preprosto čakajo večno. Medtem ko se lahko druge niti še naprej izvajajo, boste sčasoma morali ubiti program, ga znova zagnati in upati, da se ne bo znova zataknil.

Testiranje zastojev je težko, saj so zastoji odvisni od časa, obremenitve in okolja, zato se lahko zgodijo redko ali le v določenih okoliščinah. Koda ima lahko potencialno zastoj, kot je seznam 1, vendar ne kaže zastoja, dokler se ne zgodi neka kombinacija naključnih in nenaključnih dogodkov, na primer program, ki je izpostavljen določeni stopnji obremenitve, deluje v določeni konfiguraciji strojne opreme ali je izpostavljen določeni kombinacija uporabnikovih dejanj in okoljskih razmer. Zastoji so podobni časovnim bombam, ki čakajo, da eksplodirajo v naši kodi; ko to storijo, naši programi preprosto visijo.

Neenotno zaporedje ključavnic povzroča blokade

Na srečo lahko za pridobitev ključavnice postavimo razmeroma preprosto zahtevo, ki lahko prepreči blokade pri sinhronizaciji. Metode s seznama 1 imajo potencialno blokado, ker vsaka metoda pridobi dve ključavnici v drugačnem vrstnem redu. Če bi bil seznam 1 napisan tako, da je vsaka metoda dobila dve ključavnici v istem vrstnem redu, se dve ali več niti, ki izvajata te metode, ne bi mogli ustaviti, ne glede na čas ali druge zunanje dejavnike, ker nobena nit ne more pridobiti druge ključavnice, ne da bi že držala najprej. Če lahko zagotovite, da bodo ključavnice vedno pridobljene v doslednem vrstnem redu, potem vaš program ne bo zablokiral.

Zastoji niso vedno tako očitni

Ko ste spoznani s pomembnostjo naročanja ključavnic, lahko zlahka prepoznate težavo s seznama 1. Vendar se lahko analogni problemi izkažejo za manj očitne: morda sta obe metodi v ločenih razredih ali pa se vpletene ključavnice pridobijo implicitno s klicanjem sinhroniziranih metod namesto izrecno prek sinhroniziranega bloka. Razmislite o teh dveh sodelujočih razredih, Model in Pogled, v poenostavljenem okviru MVC (Model-View-Controller):

Seznam 2. Bolj prefinjena potencialna zastoj pri sinhronizaciji

 model javnega razreda {private View myView; javna sinhronizirana praznina updateModel (Object someArg) {doSomething (someArg); myView.somethingChanged (); } javni sinhronizirani objekt getSomething () {return someMethod (); }} javni razred {zasebni model underlyingModel; javna sinhronizirana void somethingChanged () {doSomething (); } javna sinhronizirana praznina updateView () {Objekt o = myModel.getSomething (); }} 

Seznam 2 ima dva sodelujoča predmeta, ki imata sinhronizirane metode; vsak objekt pokliče sinhronizirane metode drugega. Ta situacija je podobna seznamu 1 - dve metodi pridobita ključavnici na istih dveh objektih, vendar v različnih vrstnih redih. Vendar je nedosledno urejanje ključavnic v tem primeru veliko manj očitno kot v seznamu 1, ker je pridobivanje ključavnice implicitni del klica metode. Če ena nit pokliče Model.updateModel () medtem ko druga nit istočasno pokliče View.updateView (), prva nit bi lahko dobila Modelje zaklenjen in počakajte na Pogledje zaklenjen, medtem ko drugi pridobi Pogledje zaklenjen in večno čaka na Modelje zaklenjena.

Možnost sinhronizacijskega zastoja lahko pokopljete še globlje. Razmislite o tem primeru: Način imate za prenos sredstev z enega računa na drugega. Pred prenosom želite pridobiti ključavnice na obeh računih, da zagotovite, da je prenos atomski. Razmislite o tej neškodljivi izvedbi:

Seznam 3. Še bolj subtilna potencialna zastoj sinhronizacije

 public void transferMoney (Account fromAccount, Account toAccount, DollarAmount amountToTransfer) {sinhronizirano (fromAccount) {sinhronizirano (toAccount) {if (fromAccount.hasSufficientBalance (amountToTransfer) {fromAccount.debit (amountToTransofer); } 

Tudi če vse metode, ki delujejo na dveh ali več računih, uporabljajo isti vrstni red, seznam 3 vsebuje semena iste težave z mrtvo točko kot seznama 1 in 2, vendar na še bolj subtilen način. Razmislite, kaj se zgodi, ko se nit A zažene:

 transferMoney (accountOne, accountTwo, znesek); 

Medtem ko nit B izvaja:

 transferMoney (accountTwo, accountOne, anotherAmount); 

Spet obe niti poskušata pridobiti isti dve ključavnici, vendar v različnih vrstnih razredih; tveganje mrtve točke se še vedno kaže, vendar v precej manj očitni obliki.

Kako se izogniti zastojem

Eden najboljših načinov za preprečitev blokade je izogibanje pridobivanju več kot ene ključavnice hkrati, kar je pogosto praktično. Če pa to ni mogoče, potrebujete strategijo, ki zagotavlja, da pridobite več ključavnic v doslednem, definiranem vrstnem redu.

Odvisno od tega, kako vaš program uporablja ključavnice, morda ne bo težko zagotoviti doslednega vrstnega reda zaklepanja. V nekaterih programih, na primer v seznamu 1, so vse kritične ključavnice, ki bi lahko sodelovale pri večkratnem zaklepanju, črpane iz majhnega nabora enotnih elementov zaklepanja. V tem primeru lahko nabor ključavnic določite vrstni red pridobivanja ključavnic in zagotovite, da ključavnice vedno pridobite v tem vrstnem redu. Ko je vrstni red zaklepanja definiran, ga je treba preprosto dokumentirati, da spodbuja dosledno uporabo v celotnem programu.

Skrčite sinhronizirane bloke, da se izognete večkratnemu zaklepanju

Na seznamu 2 se težava zapleta, ker se zaradi klica sinhronizirane metode ključavnice pridobijo implicitno. Običajno se lahko izognete vrstam potencialnih zastojev, ki izhajajo iz primerov, kot je seznam 2, tako da omejite obseg sinhronizacije na čim manjši blok. Ali Model.updateModel () res je treba držati Model zakleni, medtem ko kliče View.somethingChanged ()? Pogosto ne; celotna metoda je bila verjetno sinhronizirana kot bližnjica, ne pa zato, ker je bilo treba sinhronizirati celotno metodo. Če pa sinhronizirane metode zamenjate z manjšimi sinhroniziranimi bloki znotraj metode, morate to vedenje zaklepanja dokumentirati kot del Javadoca metode. Klicatelji morajo vedeti, da lahko varno pokličejo metodo brez zunanje sinhronizacije. Klicatelji bi morali poznati tudi vedenje metode zaklepanja, da lahko zagotovijo, da se ključavnice pridobijo v doslednem vrstnem redu.

Prefinjenejša tehnika naročanja ključavnic

V drugih primerih, kot je primer bančnega računa na seznamu 3, uporaba pravila fiksnega reda postane še bolj zapletena; določiti morate skupno naročanje nabora predmetov, primernih za zaklepanje, in s tem vrstnim redom izbrati zaporedje pridobivanja ključavnice. To se sliši neurejeno, v resnici pa je preprosto. Seznam 4 ponazarja to tehniko; za sprožitev naročila uporablja številčno številko računa račun predmetov. (Če predmet, ki ga želite zakleniti, nima naravne lastnosti identitete, kot je številka računa, lahko uporabite Object.identityHashCode () namesto tega ustvari eno.)

Seznam 4. Uporabite naročanje za pridobivanje ključavnic v določenem zaporedju

 javna void transferMoney (Račun izAccount, Account toAccount, DollarAmount amountToTransfer) {Account firstLock, secondLock; če (fromAccount.accountNumber () == toAccount.accountNumber ()) vrže novo izjemo ("Ne morem prenesti z računa na sebe"); sicer če (fromAccount.accountNumber () <toAccount.accountNumber ()) {firstLock = fromAccount; secondLock = toAccount; } else {firstLock = toAccount; secondLock = fromAccount; } sinhronizirano (firstLock) {sinhronizirano (secondLock) {if (fromAccount.hasSufficientBalance (amountToTransfer) {fromAccount.debit (amountToTransfer); toAccount.credit (amountToTransfer);}}}} 

Zdaj vrstni red, v katerem so računi navedeni v klicu Prenesi denar() ni pomembno; ključavnice se vedno pridobijo v istem vrstnem redu.

Najpomembnejši del: Dokumentacija

Ključni - a pogosto spregledan - element vsake strategije zaklepanja je dokumentacija. Na žalost se tudi v primerih, ko je veliko pozornosti namenjeno oblikovanju strategije zaklepanja, pogosto veliko manj truda za njeno dokumentiranje. Če vaš program uporablja majhen nabor enojnih ključavnic, morate čim bolj jasno dokumentirati svoje predpostavke o naročanju ključavnic, da bodo bodoči vzdrževalci lahko izpolnili zahteve glede naročanja ključavnic. Če mora metoda pridobiti ključavnico, da lahko opravlja svojo funkcijo, ali jo je treba poklicati s posebno zadržano tipko, mora Javadoc metode to upoštevati. Tako bodo bodoči razvijalci vedeli, da lahko klicanje določene metode pomeni pridobitev ključavnice.

Le malo programov ali knjižnic razredov ustrezno dokumentira njihovo zaklepanje. Vsaka metoda mora dokumentirati ključavnice, ki jih pridobi, in ali morajo klicatelji držati ključavnico, da varno pokličejo metodo. Poleg tega bi morali razredi dokumentirati, ali so ali pod kakšnimi pogoji varni za nit.

Osredotočite se na vedenje zaklepanja v času načrtovanja

Ker zastoji pogosto niso očitni in se pojavijo redko in nepredvidljivo, lahko povzročijo resne težave v programih Java. Če ste pozorni na vedenje zaklepanja vašega programa v času načrtovanja in določite pravila, kdaj in kako pridobiti več ključavnic, lahko znatno zmanjšate verjetnost blokad. Ne pozabite skrbno dokumentirati pravil pridobivanja zaklepanja programa in njegove uporabe sinhronizacije; čas, porabljen za dokumentiranje preprostih predpostavk zaklepanja, se bo obrestoval tako, da bo pozneje močno zmanjšal možnost blokade in drugih težav s hkratnostjo.

Brian Goetz je profesionalni razvijalec programske opreme z več kot 15-letnimi izkušnjami. Je glavni svetovalec v podjetju Quiotix, podjetju za razvoj programske opreme in svetovanje s sedežem v Los Altosu v Kaliforniji.
$config[zx-auto] not found$config[zx-overlay] not found