Programovanie

Zabráňte zablokovaniu synchronizácie

V mojom predchádzajúcom článku „Dvojitá kontrola blokovania: Chytré, ale zlomené“ (JavaWorld, Február 2001), opísal som, ako je niekoľko bežných postupov na zabránenie synchronizácii v skutočnosti nebezpečných, a odporučil som stratégiu „Ak máte pochybnosti, synchronizujte“. Spravidla by ste mali synchronizovať vždy, keď čítate ľubovoľnú premennú, ktorá mohla byť predtým napísaná iným vláknom, alebo kedykoľvek píšete ľubovoľnú premennú, ktorá by mohla byť následne načítaná iným vláknom. Okrem toho, zatiaľ čo synchronizácia nesie výkonnostný trest, pokuta spojená s nekontrolovanou synchronizáciou nie je taká veľká, ako naznačujú niektoré zdroje, a stabilne sa znižovala s každou následnou implementáciou JVM. Zdá sa teda, že v súčasnosti existuje menej dôvodov ako kedykoľvek predtým vyhnúť sa synchronizácii. S nadmernou synchronizáciou je však spojené ďalšie riziko: zablokovanie.

Čo je zablokovanie?

Hovoríme, že množina procesov alebo vlákien je uviaznutý na mŕtvom bode keď každé vlákno čaká na udalosť, ktorú môže spôsobiť iba ďalší proces v množine. Ďalším spôsobom, ako ilustrovať zablokovanie, je zostavenie smerovaného grafu, ktorého vrcholy sú vlákna alebo procesy a ktorých hrany predstavujú vzťah „čaká na“. Ak tento graf obsahuje cyklus, systém je zablokovaný. Pokiaľ nie je systém navrhnutý na zotavenie po zablokovaní, zablokovanie spôsobí zablokovanie programu alebo systému.

Blokovanie synchronizácie v programoch Java

Zablokovanie sa môže v Jave vyskytnúť, pretože synchronizované kľúčové slovo spôsobí blokovanie vykonávajúceho vlákna počas čakania na zámok alebo monitor priradený k zadanému objektu. Pretože vlákno už môže obsahovať zámky spojené s inými objektmi, dve vlákna mohli každý čakať, kým druhý uvoľní zámok; v takom prípade nakoniec čakajú večne. Nasledujúci príklad ukazuje množinu metód, ktoré majú potenciál zablokovania. Obe metódy získavajú zámky na dvoch zámkových objektoch, cacheLock a tableLock, skôr ako budú pokračovať. V tomto príklade sú objekty fungujúce ako zámky globálnymi (statickými) premennými, čo je bežná technika na zjednodušenie správania uzamykania aplikácií vykonaním uzamknutia na hrubšej úrovni zrnitosti:

Zoznam 1. Potenciálne zablokovanie synchronizácie

 verejný statický objekt cacheLock = nový objekt (); verejný statický objekt tableLock = nový objekt (); ... public void oneMethod () {synchronized (cacheLock) {synchronized (tableLock) {doSomething (); }}} public void anotherMethod () {synchronized (tableLock) {synchronized (cacheLock) {doSomethingElse (); }}} 

Teraz si predstavte, že vlákno A volá oneMethod () zatiaľ čo vlákno B súčasne volá anotherMethod (). Ďalej si predstavte, že vlákno A získava zámok cacheLocka súčasne vlákno B získa zámok tableLock. Teraz sú vlákna zablokované: ani jedno vlákno sa nevzdá svojho zámku, kým nezíska druhý zámok, ale ani jedno nebude schopné získať druhý zámok, kým sa ho druhé vlákno nevzdá. Keď dôjde k zablokovaniu Java programu, zablokované vlákna jednoducho počkajú večne. Aj keď môžu naďalej bežať ďalšie vlákna, nakoniec budete musieť program zabiť, reštartovať a dúfať, že sa znova nezasekne.

Testovanie zablokovania je zložité, pretože zablokovanie závisí od načasovania, zaťaženia a prostredia, a preto sa môže stať zriedka alebo iba za určitých okolností. Kód môže mať potenciál zablokovania, ako je výpis 1, ale nemusí vykazovať zablokovanie, kým nenastane nejaká kombinácia náhodných a náhodných udalostí, ako napríklad vystavenie programu určitej úrovni zaťaženia, spustenie určitej konfigurácie hardvéru alebo vystavenie určitému zablokovaniu. zmes akcií používateľov a podmienok prostredia. Deadlocks pripomínajú časové bomby, ktoré čakajú na výbuch v našom kóde; keď to urobia, naše programy jednoducho visia.

Nekonzistentné radenie zámkov spôsobuje zablokovanie

Našťastie môžeme na získanie zámku uložiť pomerne jednoduchú požiadavku, ktorá zabráni zablokovaniu synchronizácie. Metódy zoznamu 1 majú potenciál zablokovania, pretože každá metóda získava dva zámky v inom poradí. Ak by bol zoznam 1 napísaný tak, že každá metóda získala dva zámky v rovnakom poradí, dve alebo viac vlákien vykonávajúcich tieto metódy by sa nemohlo uviaznuť bez ohľadu na načasovanie alebo iné vonkajšie faktory, pretože žiadne vlákno nemohlo získať druhý zámok bez toho, aby už držalo najprv. Ak môžete zaručiť, že zámky sa budú vždy získavať v konzistentnom poradí, váš program nebude zablokovaný.

Zablokovanie nie je vždy také zrejmé

Keď sa naučíte dôležitosť objednávania zámkov, môžete ľahko rozpoznať problém v zozname 1. Analogické problémy sa však môžu javiť ako menej zrejmé: možno sú tieto dve metódy umiestnené v samostatných triedach, alebo je možné, že príslušné zámky sa získajú implicitne volaním synchronizovaných metód namiesto explicitne prostredníctvom synchronizovaného bloku. Zvážte tieto dve spolupracujúce triedy, Model a vyhliadka, v zjednodušenom rámci MVC (Model-View-Controller):

Zoznam 2. Jemnejšie zablokovanie synchronizácie potenciálu

 public class Model {private View myView; public synchronized void updateModel (Object someArg) {doSomething (someArg); myView.somethingChanged (); } verejný synchronizovaný objekt getSomething () {return someMethod (); }} verejná trieda Zobraziť {súkromný model podkladovýModel; public synchronized void somethingChanged () {doSomething (); } verejná synchronizovaná void updateView () {Object o = myModel.getSomething (); }} 

Výpis 2 má dva spolupracujúce objekty, ktoré majú synchronizované metódy; každý objekt volá synchronizované metódy toho druhého. Táto situácia pripomína zoznam 1 - dve metódy získavajú zámky na rovnakých dvoch objektoch, ale v rôznych poradiach. Avšak nekonzistentné usporiadanie zámku v tomto príklade je oveľa menej zrejmé ako v zozname 1, pretože akvizícia zámku je implicitnou súčasťou volania metódy. Ak volá jedno vlákno Model.updateModel () zatiaľ čo iné vlákno súčasne volá View.updateView (), prvé vlákno mohlo získať Modelje zámok a čakať na vyhliadkazámok, zatiaľ čo druhý získa vyhliadkaje zámok a čaká večne na internet Modelzámok.

Potenciál zablokovania synchronizácie môžete pochovať ešte hlbšie. Zvážte tento príklad: Máte spôsob prevodu finančných prostriedkov z jedného účtu na druhý. Pred uskutočnením prevodu chcete získať zámky na oboch účtoch, aby ste sa uistili, že je prenos atómový. Zvážte túto neškodne vyzerajúcu implementáciu:

Zoznam 3. Ešte jemnejšie potenciálne zablokovanie synchronizácie

 public void transferMoney (Účet z účtu, Účet na účet, DollarAmount sumaToTransfer) {synchronizovaný (z účtu) {synchronizovaný (na účet) {if (z účtu.hasSufficientBalance (sumaToTransfer) {odAccount.debit (sumaToTransfer); toAccount.credit (sumaToTransfer); } 

Aj keď všetky metódy, ktoré fungujú na dvoch alebo viacerých účtoch, používajú rovnaké usporiadanie, výpis 3 obsahuje zárodky rovnakého problému zablokovania ako výpisy 1 a 2, ale ešte jemnejším spôsobom. Zvážte, čo sa stane, keď sa vlákno A vykoná:

 transferMoney (accountOne, accountTwo, čiastka); 

V rovnakom čase vlákno B vykoná:

 transferMoney (accountTwo, accountOne, anotherAmount); 

Dve vlákna sa opäť pokúšajú získať tie isté dva zámky, ale v odlišnom poradí; stále sa objavuje riziko zablokovania, ale v oveľa menej zjavnej podobe.

Ako sa vyhnúť zablokovaniu

Jedným z najlepších spôsobov, ako zabrániť možnému uviaznutiu, je vyhnúť sa získaniu viac ako jedného zámku naraz, čo je často praktické. Ak to však nie je možné, potrebujete stratégiu, ktorá zabezpečí získanie viacerých zámkov v konzistentnom a definovanom poradí.

V závislosti na tom, ako váš program používa zámky, nemusí byť komplikované zabezpečiť, aby ste používali konzistentné poradie uzamknutia. V niektorých programoch, napríklad v zozname 1, sú všetky kritické zámky, ktoré by sa mohli podieľať na viacnásobnom uzamknutí, nakreslené z malej množiny jednorázových zámkových objektov. V takom prípade môžete na množine zámkov definovať poradie získavania zámkov a zabezpečiť, aby ste zámky získavali vždy v tomto poradí. Len čo je definované poradie uzamknutia, je potrebné ho jednoducho dobre zdokumentovať, aby sa podporilo jeho dôsledné používanie v celom programe.

Zmenšujte synchronizované bloky, aby ste sa vyhli viacnásobnému uzamknutiu

V zozname 2 sa problém komplikuje, pretože v dôsledku volania synchronizovanej metódy sa zámky získavajú implicitne. Spravidla sa môžete vyhnúť druhu možných zablokovaní, ktoré vznikajú v prípadoch, ako je výpis 2, zúžením rozsahu synchronizácie na čo najmenší blok. Má Model.updateModel () naozaj treba držať Model zamknúť, kým volá View.somethingChanged ()? Často nie; celá metóda bola pravdepodobne synchronizovaná ako skratka, a nie preto, že by bolo treba synchronizovať celú metódu. Ak však vo vnútri metódy nahradíte synchronizované metódy menšími synchronizovanými blokmi, musíte toto správanie uzamknutia zdokumentovať ako súčasť Javadocu metódy. Volajúci musia vedieť, že môžu bezpečne volať metódu bez externej synchronizácie. Volajúci by tiež mali poznať správanie uzamykania metódy, aby mohli zabezpečiť, že zámky sa získavajú v konzistentnom poradí.

Prepracovanejšia technika objednávania zámku

V iných situáciách, ako napríklad príklad bankového účtu v zozname 3, je použitie pravidla pevnej objednávky ešte komplikovanejšie; musíte definovať celkové poradie na množine objektov vhodných na uzamknutie a pomocou tohto usporiadania zvoliť postupnosť získania zámku. Znie to chaoticky, ale v skutočnosti je to priame. Výpočet 4 ilustruje túto techniku; na vyvolanie objednávky používa číselné číslo účtu Účet predmety. (Ak objektu, ktorý potrebujete zamknúť, chýba vlastnosť prirodzenej identity, napríklad číslo účtu, môžete použiť Object.identityHashCode () namiesto toho vygenerujte jeden.)

Zoznam 4. Pomocou objednávky získajte zámky v pevnom poradí

 public void transferMoney (Účet z účtu, účet na účet, DollarAmount amountToTransfer) {účet firstLock, secondLock; if (fromAccount.accountNumber () == toAccount.accountNumber ()) hodiť novú Výnimku ("Nemožno previesť z účtu na seba"); else if (fromAccount.accountNumber () <toAccount.accountNumber ()) {firstLock = fromAccount; secondLock = toAccount; } else {firstLock = toAccount; secondLock = zÚčet; } synchronized (firstLock) {synchronized (secondLock) {if (fromAccount.hasSufficientBalance (amountToTransfer) {fromAccount.debit (amountToTransfer); toAccount.credit (amountToTransfer);}}} 

Teraz poradie, v ktorom sú účty uvedené vo výzve transferMoney () na tom nezáleží; zámky sa získavajú vždy v rovnakom poradí.

Najdôležitejšia časť: Dokumentácia

Kritickým - ale často prehliadaným - prvkom akejkoľvek stratégie uzamykania je dokumentácia. Bohužiaľ, aj v prípadoch, keď je návrh zámkovej stratégie venovaný veľká pozornosť, sa na jej zdokumentovanie často vynaloží oveľa menšie úsilie. Ak váš program používa malú sadu singletonových zámkov, mali by ste čo najjasnejšie zdokumentovať svoje predpoklady radenia zámkov, aby budúci správcovia mohli splniť požiadavky na radenie zámkov. Ak metóda musí získať zámok, aby mohla vykonávať svoju funkciu, alebo sa musí volať so zadržaným konkrétnym zámkom, Javadoc tejto metódy by si mal túto skutočnosť všimnúť. Budúci vývojári tak budú vedieť, že volanie danej metódy môže znamenať získanie zámku.

Len málo programov alebo knižníc tried adekvátne dokumentuje ich použitie uzamknutia. Každá metóda by mala minimálne dokumentovať zámky, ktoré získava, a to, či musia volajúci držať zámok, aby bolo možné metódu bezpečne zavolať. Okrem toho by triedy mali dokumentovať, či sú alebo nie sú chránené vláknami alebo za akých podmienok.

Zamerajte sa na správanie zamykania v čase návrhu

Pretože zablokovania často nie sú zrejmé a vyskytujú sa zriedkavo a nepredvídateľne, môžu v programoch Java spôsobiť vážne problémy. Venovaním pozornosti priebehu uzamykania vášho programu v čase návrhu a definovaniu pravidiel, kedy a ako získať viac zámkov, môžete značne znížiť pravdepodobnosť zablokovania. Nezabudnite starostlivo zdokumentovať pravidlá získavania zámku vášho programu a jeho použitie synchronizácie; čas strávený zdokumentovaním jednoduchých predpokladov uzamknutia sa vyplatí výrazným znížením šance na uviaznutie a ďalšie problémy so súbežnosťou neskôr.

Brian Goetz je profesionálny vývojár softvéru s viac ako 15-ročnými skúsenosťami. Je hlavným konzultantom v spoločnosti Quiotix, softvérovej a konzultačnej spoločnosti so sídlom v Los Altos v Kalifornii.
$config[zx-auto] not found$config[zx-overlay] not found