Programavimas

Venkite sinchronizavimo aklaviečių

Mano ankstesniame straipsnyje „Dvigubai patikrintas užraktas: sumanus, bet sulaužytas“ („JavaWorld“, 2001 m. Vasario mėn.) Aprašiau, kaip kelios įprastos sinchronizacijos išvengimo technikos iš tikrųjų yra nesaugios, ir rekomendavau strategiją „Jei kyla abejonių, sinchronizuokite“. Apskritai turėtumėte sinchronizuoti, kai skaitote kintamąjį, kurį anksčiau galėjo parašyti kita gija, arba kai rašote kintamąjį, kurį vėliau galėjo perskaityti kita gija. Be to, nors už sinchronizavimą baudžiama už našumą, bauda, ​​susijusi su netrikdomu sinchronizavimu, nėra tokia didelė, kaip siūlė kai kurie šaltiniai, ir nuosekliai sumažėjo kiekvieną kartą įgyvendinant JVM. Taigi atrodo, kad dabar yra mažiau nei bet kada priežasčių vengti sinchronizavimo. Tačiau kita rizika yra susijusi su per dideliu sinchronizavimu: aklavietė.

Kas yra aklavietė?

Mes sakome, kad procesų ar gijų rinkinys yra aklavietėje kai kiekviena gija laukia įvykio, kurį gali sukelti tik kitas rinkinio procesas. Kitas būdas parodyti aklavietę yra sukurti nukreiptą grafiką, kurio viršūnės yra gijos ar procesai, o jo kraštai atspindi „laukia“ santykį. Jei šiame grafike yra ciklas, sistema yra užstrigusi. Jei sistema nėra sukurta atsigauti po aklavietės, dėl aklavietės programa ar sistema pakimba.

Sinchronizavimo aklavietės „Java“ programose

„Java“ gali atsirasti strigčių, nes sinchronizuotas raktinis žodis priverčia vykdomąjį giją blokuoti laukiant užrakto ar monitoriaus, susieto su nurodytu objektu. Kadangi gijoje jau gali būti spynos, susijusios su kitais objektais, dvi gijos gali laukti, kol kitos atleis spyną; tokiu atveju jie liks amžinai laukti. Šiame pavyzdyje parodyta metodų, galinčių atsidurti aklavietėje, rinkinys. Abiem būdais užrakinami du užrakto objektai, cacheLock ir „tableLock“, kol jie tęsis. Šiame pavyzdyje objektai, veikiantys kaip užraktai, yra visuotiniai (statiniai) kintamieji - įprasta taikomųjų programų užrakinimo elgesio supaprastinimo technika, atliekant užrakinimą šiurkščiau:

Sąrašas 1. Galimas sinchronizavimo aklavietė

 public static Object cacheLock = new Object (); public static Object tableLock = new Object (); ... public void oneMethod () {synchronized (cacheLock) {synchronized (tableLock) {doSomething (); }}} public anuliuoti kitą metodą () {sinchronizuotas (tableLock) {sinchronizuotas (cacheLock) {doSomethingElse (); }}} 

Dabar įsivaizduokite, kad skambina A gija „oneMethod“ () o B siūlas vienu metu skambina kitas metodas (). Įsivaizduokite toliau, kad sriegis A įgyja spyną cacheLockir tuo pačiu metu sriegis B įgauna spyną „tableLock“. Dabar siūlai yra užstrigę: nė vienas siūlas neatsisakys savo užrakto, kol neįgaus kito užrakto, bet nė vienas negalės įsigyti kito užrakto, kol kitas siūlas jo neatsisakys. Kai „Java“ programa užstringa, aklavietės gijos tiesiog laukia amžinai. Nors kitos gijos gali ir toliau veikti, galiausiai turėsite užmušti programą, paleisti ją iš naujo ir tikėtis, kad ji vėl neužstrigs.

Patikrinti aklavietes yra sunku, nes aklavietės priklauso nuo laiko, apkrovos ir aplinkos, todėl gali įvykti nedažnai arba tik esant tam tikroms aplinkybėms. Kodas gali patekti į aklavietę, pvz., 1 sąrašas, tačiau aklavietė gali būti rodoma tik tada, kai įvyksta tam tikri atsitiktinių ir atsitiktinių įvykių deriniai, pvz., Programa yra veikiama tam tikru apkrovos lygiu, veikia pagal tam tikrą aparatūros konfigūraciją arba yra veikiama tam tikros vartotojų veiksmų ir aplinkos sąlygų derinys. Aklavietės panašios į bombas, kurios laukia sprogimo mūsų kode; kai jie tai daro, mūsų programos tiesiog pakimba.

Nenuoseklus spynų užsakymas sukelia aklavietes

Laimei, mes galime nustatyti gana paprastą spynos įsigijimo reikalavimą, kuris gali užkirsti kelią sinchronizacijos aklavietėms. 1 sąrašo metodai gali atsidurti aklavietėje, nes kiekvienas metodas įgyja du užraktus kita tvarka. Jei 1 sąrašas buvo parašytas taip, kad kiekvienas metodas įgijo du užraktus ta pačia tvarka, dvi ar daugiau gijų, vykdančių šiuos metodus, negalėjo patekti į aklavietę, neatsižvelgiant į laiką ar kitus išorinius veiksnius, nes nė viena gija negalėjo įsigyti antrojo užrakto, jau neturėdama Pirmas. Jei galite garantuoti, kad spynos visada bus įsigyjamos nuoseklia tvarka, jūsų programa nebus aklavietė.

Ne visada aklavietė yra tokia akivaizdi

Susipažinę su užrakto užsakymo svarba, galite lengvai atpažinti 1 sąrašo problemą. Tačiau panašios problemos gali pasirodyti ne tokios akivaizdžios: galbūt abu metodai yra atskirose klasėse, o gal užrakinti spynos yra įgyjamos netiesiogiai iškviečiant sinchronizuotus metodus, o ne aiškiai per sinchronizuotą bloką. Apsvarstykite šias dvi bendradarbiaujančias klases, Modelis ir Vaizdas, supaprastintoje MVC (Model-View-Controller) sistemoje:

Sąrašas 2. Subtilesnė potencialių sinchronizavimo aklavietė

 viešosios klasės modelis {private View myView; viešas sinchronizuotas negaliojantis updateModel (objektas someArg) {doSomething (someArg); myView.somethingChanged (); } viešas sinchronizuotas objektas getSomething () {return someMethod (); }} viešoji klasė Žiūrėti {privatų modelį pagrindinį modelį; public synchronized void somethingChanged () {doSomething (); } public synchronized void updateView () {Object o = myModel.getSomething (); }} 

2 sąraše yra du bendradarbiaujantys objektai, turintys sinchronizuotus metodus; kiekvienas objektas iškviečia kito sinchronizuotus metodus. Ši situacija panaši į 1 sąrašą - dviem būdais spynos gaunamos ant tų pačių dviejų objektų, tačiau skirtinga tvarka. Tačiau nenuoseklus spynos tvarkymas šiame pavyzdyje yra daug mažiau akivaizdus nei 1 sąraše, nes užrakto įsigijimas yra numanoma metodo iškvietimo dalis. Jei skambina viena gija Model.updateModel () o kita gija vienu metu skambina View.updateView (), pirmasis siūlas galėjo gauti Modelisužraktas ir palaukite Vaizdasužraktą, o kitas gauna Vaizdasužraktas ir amžinai laukia Modelisspyna.

Sinchronizavimo aklavietę galite palaidoti dar giliau. Apsvarstykite šį pavyzdį: turite būdą pervesti lėšas iš vienos sąskaitos į kitą. Prieš atlikdami perkėlimą, norite įsigyti abiejų sąskaitų užraktus, kad įsitikintumėte, jog pervedimas yra atominis. Apsvarstykite šį nepavojingai atrodantį įgyvendinimą:

Išvardijamas 3. Dar subtilesnė potencialios sinchronizacijos aklavietė

 public void transferMoney (sąskaita išAccount, sąskaita įAccount, DollarAmount summaToTransfer) {sinchronizuota (fromAccount) {synchronized (toAccount) {if (fromAccount.hasSufficientBalance (summaToTransfer) {fromAccount.debit (summaToTransfer}); toAccount; } 

Net jei visi metodai, veikiantys dviem ar daugiau paskyrų, naudoja tą patį eiliškumą, 3 sąraše yra tos pačios aklavietės problemos, kaip ir 1 ir 2 sąrašuose, sėklos, tačiau dar subtiliau. Apsvarstykite, kas atsitinka, kai vykdoma A gija:

 transferMoney (accountOne, accountTwo, suma); 

Tuo pačiu metu B sriegis vykdo:

 transferMoney (accountTwo, accountOne, anotherAmount); 

Vėlgi, dvi gijos bando įsigyti tas pačias dvi spynas, tačiau skirtinga tvarka; aklavietės rizika vis dar kyla, tačiau daug mažiau akivaizdi forma.

Kaip išvengti aklavietės

Vienas iš geriausių būdų užkirsti kelią aklavietei yra vengti įsigyti daugiau nei vieną spyną vienu metu, o tai dažnai yra praktiška. Tačiau, jei tai neįmanoma, jums reikia strategijos, užtikrinančios, kad įsigysite kelis užraktus nuoseklia, apibrėžta tvarka.

Priklausomai nuo to, kaip jūsų programa naudoja užraktus, gali būti nesudėtinga užtikrinti, kad naudojate pastovią užrakinimo tvarką. Kai kuriose programose, pvz., 1 sąraše, visos kritinės spynos, kurios gali dalyvauti keliuose užraktuose, yra sudaromos iš nedidelio pavienio užrakto objektų rinkinio. Tokiu atveju galite nustatyti spynų įsigijimo tvarką spynų rinkinyje ir užtikrinti, kad spynos visada įsigytumėte tokia tvarka. Nustačius užrakto tvarką, ją tiesiog reikia gerai dokumentuoti, kad būtų skatinamas nuoseklus naudojimasis visa programa.

Sumažinkite sinchronizuotus blokus, kad išvengtumėte daugkartinio užrakinimo

2 sąraše problema tampa vis sudėtingesnė, nes dėl sinchronizuoto metodo iškvietimo spynos įgaunamos netiesiogiai. Paprastai galite išvengti galimų aklaviečių, atsirandančių dėl tokių atvejų kaip 2 sąrašas, susiaurindami sinchronizavimo sritį iki kuo mažesnio bloko. Ar Model.updateModel () tikrai reikia laikyti Modelis užrakinti, kol jis skambina View.somethingChanged ()? Dažnai taip nėra; visas metodas greičiausiai buvo sinchronizuotas kaip spartusis klavišas, o ne todėl, kad reikėjo sinchronizuoti visą metodą. Tačiau jei pakeisite sinchronizuotus metodus mažesniais sinchronizuotais blokais metodo viduje, turite užfiksuoti šį užrakto elgesį kaip metodo „Javadoc“ dalį. Skambintojai turi žinoti, kad jie gali saugiai paskambinti metodui be išorinės sinchronizacijos. Skambintojai taip pat turėtų žinoti metodo užrakinimo elgseną, kad galėtų užtikrinti, kad spynos būtų įgyjamos nuosekliomis eilės tvarka.

Įmantresnė spynų užsakymo technika

Kitose situacijose, pvz., „3 sąrašo“ banko sąskaitos pavyzdyje, fiksuoto pavedimo taisyklės taikymas tampa dar sudėtingesnis; turite nustatyti bendrą objektų, kuriuos galima užrakinti, rinkinį ir naudoti šį užsakymą, kad pasirinktumėte užrakto įsigijimo seką. Tai skamba netvarkingai, bet iš tikrųjų yra nesudėtinga. 4 sąrašas iliustruoja tą techniką; jis naudoja skaitinį sąskaitos numerį, kad paskatintų užsakymą Sąskaita objektai. (Jei objekte, kurį reikia užrakinti, trūksta natūralios tapatybės savybės, tokios kaip sąskaitos numeris, galite naudoti Object.identityHashCode () metodą.

Sąrašas 4. Naudokite užsakymą, kad gautumėte fiksuotas fiksuotas spynas

 public void transferMoney (sąskaita išAccount, sąskaita įSąskaita, DollarAmount summaToTransfer) {sąskaita firstLock, secondLock; if (fromAccount.accountNumber () == toAccount.accountNumber ()) meta naują išimtį ("Negalima perkelti iš paskyros į save"); else if (išSąskaita.sąskaitos numeris () <įSąskaita.sąskaitos numeris ()) {firstLock = išSąskaita; secondLock = į sąskaitą; } else {firstLock = toAccount; secondLock = iš sąskaitos; } synchronized (firstLock) {synchronized (secondLock) {if (fromAccount.hasSufficientBalance (summaToTransfer) {fromAccount.debit (summaToTransfer); toAccount.credit (summaToTransfer);}}}} 

Dabar tvarka, kuria sąskaitos nurodomos kvietime pervesti pinigus() nesvarbu; spynos visada įsigyjamos ta pačia tvarka.

Svarbiausia dalis: Dokumentacija

Svarbiausias bet dažnai užmirštamas bet kurios užrakinimo strategijos elementas yra dokumentacija. Deja, net tais atvejais, kai kuriama daug dėmesio kuriant užrakto strategiją, dažnai jos dokumentavimui skiriama daug mažiau pastangų. Jei jūsų programoje naudojamas nedidelis pavienių spynų rinkinys, turėtumėte kuo aiškiau dokumentuoti savo spynų užsakymo prielaidas, kad būsimi prižiūrėtojai galėtų įvykdyti spynų užsakymo reikalavimus. Jei metodas turi įsigyti spyną, kad galėtų atlikti savo funkciją, arba jis turi būti iškviestas laikant tam tikrą spyną, metodo „Javadoc“ turėtų atkreipti dėmesį į tai. Tokiu būdu būsimi kūrėjai žinos, kad tam tikro metodo iškvietimas gali reikėti įsigyti užraktą.

Nedaug programų ar klasių bibliotekų tinkamai dokumentuoja jų užrakinimo naudojimą. Kiekvienas metodas turėtų bent dokumentuoti užraktus, kuriuos jis įgijo, ir tai, ar skambinantys asmenys turi turėti spyną, kad saugiai iškviestų metodą. Be to, klasės turėtų dokumentuoti, ar jos yra saugios, ar ne, ar kokiomis sąlygomis.

Sutelkite dėmesį į užrakinimo elgesį projektavimo metu

Kadangi aklavietės dažnai nėra akivaizdžios ir įvyksta retai ir nenuspėjamai, jos gali sukelti rimtų problemų „Java“ programose. Atkreipdami dėmesį į programos užrakinimo elgseną projektavimo metu ir apibrėždami taisykles, kada ir kaip įsigyti kelias spynas, galite žymiai sumažinti aklaviečių tikimybę. Nepamirškite atidžiai dokumentuoti savo programos užrakto gavimo taisyklių ir sinchronizavimo naudojimo; laikas, praleistas dokumentuojant paprastas fiksavimo prielaidas, pasiteisins, nes vėliau labai sumažės aklavietės ir kitų lygiagretumo problemų tikimybė.

Brianas Goetzas yra profesionalus programinės įrangos kūrėjas, turintis daugiau nei 15 metų patirtį. Jis yra pagrindinis programinės įrangos kūrimo ir konsultavimo įmonės „Quiotix“, įsikūrusios Los Altos mieste, Kalifornijoje, konsultantas.