Programavimas

Dvigubai patikrintas užraktas: sumanus, bet sugedęs

Iš labai vertinamų „Java“ stiliaus elementai į „JavaWorld“ (žr. „Java“ patarimą 67), daugelis geranoriškų „Java“ guru skatina naudoti dvigubai patikrintą užrakinimo (DCL) idiomą. Yra tik viena problema - ši protinga, atrodanti idioma gali neveikti.

Dvigubai patikrintas užrakinimas gali būti pavojingas jūsų kodui!

Šią savaitę „JavaWorld“ daugiausia dėmesio skiriama dvigubai patikrintos užrakto idėjos pavojams. Skaitykite daugiau apie tai, kaip šis, atrodytų, nekenksmingas spartusis klavišas, gali sugadinti jūsų kodą:
  • "Įspėjimas! Sriegimas daugiaprocesoriniame pasaulyje", - Allenas Holubas
  • Dvigubai patikrintas užraktas: sumanus, bet sugedęs “, - Brianas Goetzas
  • Norėdami daugiau sužinoti apie dvigubai patikrintą užraktą, eikite į Allen Holub Programavimo teorijos ir praktikos aptarimas

Kas yra DCL?

DCL idioma buvo sukurta palaikyti tingų inicializavimą, kuris įvyksta, kai klasė atideda nuosavybės objekto inicijavimą, kol to tikrai nereikia:

klasė SomeClass {privataus šaltinio išteklius = null; viešasis šaltinis getResource () {if (resursas == null) šaltinis = naujas šaltinis (); grąžinimo šaltinis; }} 

Kodėl norėtumėte atidėti inicijavimą? Galbūt sukuriant Ištekliai yra brangi operacija, o SomeClass gali iš tikrųjų neskambinti getResource () bet kuriame bėgime. Tokiu atveju galite išvengti Ištekliai visiškai. Nepaisant to, SomeClass objektą galima sukurti greičiau, jei jam nereikia kurti ir Ištekliai statybos metu. Atidėjus kai kurias inicializavimo operacijas, kol vartotojui iš tikrųjų reikės jų rezultatų, programos gali padėti greičiau paleisti.

Ką daryti, jei bandysite naudoti SomeClass daugialypėje programoje? Tada atsiranda varžybų sąlyga: dvi gijos vienu metu galėtų atlikti bandymą, kad pamatytumėte, ar išteklių yra niekinis ir dėl to inicijuokite išteklių du kartus. Daugiasluoksnėje aplinkoje turėtumėte deklaruoti getResource () būti sinchronizuotas.

Deja, sinchronizuoti metodai veikia daug lėčiau - net 100 kartų lėčiau - nei įprasti nesinchronizuoti metodai. Viena iš tingaus inicijavimo motyvų yra efektyvumas, tačiau atrodo, kad norint greičiau paleisti programą, paleidus programą turite sutikti su lėtesniu vykdymo laiku. Tai neatrodo puikus kompromisas.

DCL siekia suteikti mums geriausią iš abiejų pasaulių. Naudojant DCL, getResource () metodas atrodytų taip:

klasė SomeClass {privataus šaltinio išteklius = null; public Resource getResource () {if (resursas == null) {sinchronizuotas {if (resursas == null) resursas = naujas Ištekliai (); }} grąžinimo šaltinis; }} 

Po pirmo skambučio į getResource (), išteklių jau inicializuotas, todėl išvengiama sinchronizavimo įvykio dažniausiai naudojamame kodo kelyje. DCL taip pat apsaugo nuo varžybų būklės išteklių antrą kartą sinchronizuoto bloko viduje; tai užtikrina, kad tik viena gija bandys inicijuoti išteklių. DCL atrodo kaip protinga optimizacija, bet ji neveikia.

Susipažinkite su „Java“ atminties modeliu

Tiksliau, negarantuojama, kad DCL veiks. Norėdami suprasti, kodėl turime pažvelgti į ryšį tarp JVM ir kompiuterinės aplinkos, kurioje jis veikia. Visų pirma turime atkreipti dėmesį į „Java“ atminties modelį (JMM), apibrėžtą 17 skyriuje „Java“ kalbos specifikacija, Billas Joy, Guy Steele'as, Jamesas Goslingas ir Giladas Bracha (Addison-Wesley, 2000), kuriame išsamiai aprašoma, kaip „Java“ tvarko sąveiką tarp gijų ir atminties.

Skirtingai nuo daugumos kitų kalbų, „Java“ apibrėžia savo ryšį su pagrindine aparatine įranga naudodamas oficialų atminties modelį, kuris, tikimasi, išliks visose „Java“ platformose, suteikdamas „Java“ pažadą „Rašyti kartą, paleisk bet kur“. Palyginimui, kitoms kalboms, tokioms kaip C ir C ++, trūksta oficialaus atminties modelio; tokiomis kalbomis programos paveldi aparatinės įrangos platformos, kurioje veikia programa, atminties modelį.

Vykdant sinchroninėje (vienos gijos) aplinkoje, programos sąveika su atmintimi yra gana paprasta arba bent jau taip atrodo. Programos saugo elementus atminties vietose ir tikisi, kad jie vis tiek bus ten kitą kartą, kai bus nagrinėjamos tos atminties vietos.

Tiesą sakant, tiesa yra visiškai kitokia, tačiau sudėtinga iliuzija, kurią palaiko kompiliatorius, JVM ir aparatūra, ją slepia nuo mūsų. Nors mes manome, kad programos vykdomos nuosekliai - programos kodo nurodyta tvarka - tai ne visada įvyksta. Kompiliatoriai, procesoriai ir talpyklos gali laisvai naudotis visomis laisvėmis naudodamiesi mūsų programomis ir duomenimis, jei tik tai neturi įtakos skaičiavimo rezultatui. Pvz., Kompiliatoriai gali sugeneruoti instrukcijas kita tvarka, nei akivaizdus programos siūlomas aiškinimas, ir kintamuosius saugoti registruose, o ne atmintyje; procesoriai gali vykdyti nurodymus lygiagrečiai arba ne savo tvarka; o talpyklos gali skirtis tvarka, kuria rašoma įpareigoti pagrindinę atmintį. JMM teigia, kad visi šie įvairūs pertvarkymai ir optimizavimas yra priimtini, jei tik palaikoma aplinka tarsi serijinis semantika - ty tol, kol pasieksite tą patį rezultatą, kokį turėtumėte, jei instrukcijos būtų vykdomos griežtai nuoseklioje aplinkoje.

Kompiliatoriai, procesoriai ir talpyklos pertvarko programos operacijų seką, kad būtų pasiektas didesnis našumas. Pastaraisiais metais pastebėjome nepaprastai patobulintą skaičiavimo našumą. Nors padidėjęs procesoriaus taktinis dažnis iš esmės prisidėjo prie didesnio našumo, padidėjęs lygiagretumas (vamzdynų ir viršskaliarinių vykdymo vienetų, dinamiško komandų planavimo ir spekuliacinio vykdymo bei sudėtingų daugiapakopių atminties talpyklų pavidalu) taip pat buvo pagrindinis veiksnys. Tuo pačiu metu kompiliatorių rašymo užduotis tapo daug sudėtingesnė, nes kompiliatorius turi apsaugoti programuotoją nuo šių sudėtingumų.

Rašydami vienos gijos programas, negalite pamatyti šių įvairių instrukcijų ar atminties operacijų pertvarkymo poveikio. Tačiau naudojant daugiasluoksnes programas situacija yra visai kitokia - viena gija gali nuskaityti atminties vietas, kurias parašė kita gija. Jei gija A modifikuoja kai kuriuos kintamuosius tam tikra tvarka, nesant sinchronizavimo, gija B gali nematyti jų ta pačia tvarka - arba nematyti. Tai gali atsitikti, nes kompiliatorius pertvarkė instrukcijas arba laikinai išsaugojo kintamąjį registre ir vėliau išrašė į atmintį; arba todėl, kad procesorius vykdė instrukcijas lygiagrečiai arba kita tvarka, nei nurodyta kompiliatoriuje; arba dėl to, kad instrukcijos buvo skirtinguose atminties regionuose, o talpykla atnaujino atitinkamas pagrindinės atminties vietas kita tvarka nei ta, kuria jos buvo parašytos. Nepriklausomai nuo aplinkybių, daugiasriegės programos yra iš esmės mažiau nuspėjamos, nebent jūs aiškiai užtikrinate, kad gijos turi nuoseklų atminties vaizdą naudodamos sinchronizavimą.

Ką iš tikrųjų reiškia sinchronizavimas?

„Java“ traktuoja kiekvieną giją taip, lyg ji veiktų su savo procesoriumi su savo vietine atmintimi, kiekviena kalbėtųsi ir sinchronizuotųsi su bendra pagrindine atmintimi. Net ir vieno procesoriaus sistemoje tas modelis yra prasmingas dėl atminties talpyklų poveikio ir procesorių registrų naudojimo kintamiesiems saugoti. Kai gija modifikuoja vietą savo vietinėje atmintyje, tas modifikavimas ilgainiui turėtų pasirodyti ir pagrindinėje atmintyje, o JMM apibrėžia taisykles, kada JVM turi perduoti duomenis tarp vietinės ir pagrindinės atminties. „Java“ architektai suprato, kad pernelyg ribojantis atminties modelis labai pakenks programos veikimui. Jie bandė sukurti atminties modelį, kuris leistų programoms gerai veikti naudojant šiuolaikinę kompiuterinę aparatinę įrangą, tačiau vis tiek suteiktų garantijas, kurios leistų siūlams sąveikauti numatomais būdais.

Pagrindinis „Java“ įrankis, leidžiantis numatyti sąveiką tarp gijų, yra sinchronizuotas raktinis žodis. Daugelis programuotojų sugalvoja sinchronizuotas griežtai kalbant apie abipusės atskirties semaforos vykdymą (muteksas), kad kritinės sekcijos nebūtų vykdomos daugiau nei viena gija vienu metu. Deja, ta intuicija nevisiškai apibūdina ką sinchronizuotas reiškia.

Semantika sinchronizuotas iš tikrųjų apima abipusį vykdymo neįtraukimą pagal semaforo būseną, tačiau taip pat yra taisyklės apie sinchronizuojamos gijos sąveiką su pagrindine atmintimi. Visų pirma, spynos įsigijimas ar atleidimas sukelia a atminties barjeras - priverstinė sinchronizacija tarp vietinės gijos atminties ir pagrindinės atminties. (Kai kurie procesoriai, pvz., „Alfa“, turi aiškias mašinos instrukcijas, kaip atlikti atminties barjerus.) Kai gija išeina iš a sinchronizuotas bloką, jis atlieka rašymo barjerą - prieš atleisdamas užraktą, jis turi išvalyti visus kintamuosius, modifikuotus tame bloke, į pagrindinę atmintį. Panašiai įvedant a sinchronizuotas bloką, jis atlieka skaitymo barjerą - tarytum vietinė atmintis būtų padariusi negaliojančią, ir ji turi iš pagrindinės atminties atimti visus kintamuosius, kurie bus nurodyti bloke.

Tinkamas sinchronizavimo naudojimas garantuoja, kad viena gija numatomu būdu matys kito poveikį. Tik tada, kai A ir B gijos sinchronizuojasi tame pačiame objekte, JMM garantuoja, kad gija B matys A gijos pakeitimus ir kad A sinchronizuotas pasirodo blokas atomiškai B gijai (arba visas blokas nevykdomas, arba nė vienas iš jų nevykdo.) Be to, JMM tai užtikrina sinchronizuotas blokai, kurie sinchronizuojami tame pačiame objekte, pasirodys vykdomi ta pačia tvarka, kaip ir programoje.

Taigi, kas sulaužyta DCL?

DCL remiasi nesinchronizuotu išteklių srityje. Atrodo, kad tai yra nekenksminga, bet taip nėra. Norėdami sužinoti, kodėl, įsivaizduokite, kad sriegis A yra sinchronizuotas blokuoti, vykdyti pareiškimą resursas = naujas šaltinis (); kol siūlas B tik įeina getResource (). Apsvarstykite šio inicialo poveikį atminčiai. Atmintis naujam Ištekliai objektas bus paskirtas; statybininkas Ištekliai bus iškviestas, inicijuojant naujojo objekto narių laukus; ir laukas išteklių apie SomeClass bus priskirta nuoroda į naujai sukurtą objektą.

Tačiau, kadangi gija B nevykdo a viduje sinchronizuotas bloką, jis gali matyti šias atminties operacijas kita tvarka nei ta, kurią vykdo viena gija A. Gali būti, kad B šiuos įvykius mato tokia tvarka (o kompiliatorius taip pat gali laisvai pertvarkyti tokias instrukcijas): paskirstykite atmintį, priskirkite nuorodą į išteklių, skambinkite konstruktoriumi. Tarkime, kad gija B ateina po atminties paskirstymo ir išteklių laukas nustatytas, bet prieš iškviečiant konstruktorių. Tai mato išteklių nėra nulis, praleidžia sinchronizuotas bloką ir pateikia nuorodą į iš dalies sukonstruotą Ištekliai! Nereikia nė sakyti, kad rezultatas nėra nei laukiamas, nei norimas.

Pateikiant šį pavyzdį, daugelis žmonių iš pradžių yra skeptiški. Daugelis labai intelektualių programuotojų bandė pataisyti DCL taip, kad jis veiktų, tačiau neveikia nė viena iš šių tariamai fiksuotų versijų. Reikėtų pažymėti, kad DCL iš tikrųjų gali veikti su kai kuriomis JVM versijomis - nes nedaugelis JVM iš tikrųjų tinkamai įgyvendina JMM. Tačiau jūs nenorite, kad programų teisingumas būtų pagrįstas išsamia įgyvendinimo informacija, ypač klaidomis, būdingomis konkrečiai jūsų naudojamo JVM versijai.

Kiti lygiagretumo pavojai yra įterpti į DCL ir į bet kokią nesinchronizuotą nuorodą į atmintį, kurią parašo kita gija, net ir nekenksmingos išvaizdos skaitymai. Tarkime, kad A gija baigė inicializuoti Ištekliai ir išeina iš sinchronizuotas blokuoti, kai įeina sriegis B getResource (). Dabar Ištekliai yra visiškai inicijuotas, o gija A vietinę atmintį ištrina į pagrindinę atmintį. ištekliųLaukai gali nurodyti kitus atmintyje saugomus objektus per nario laukus, kurie taip pat bus ištrinti. Nors gija B gali pamatyti galiojančią nuorodą į naujai sukurtą Ištekliai, nes jis neatliko skaitymo barjero, vis tiek galėjo matyti pasenusias ištekliųnarių laukai.

Nepastovus nereiškia ir to, ką tu galvoji

Dažniausiai siūlomas nefiksas yra deklaruoti išteklių sritis SomeClass kaip nepastovus. Tačiau, nors JMM neleidžia rašyti nepastoviems kintamiesiems pertvarkyti vienas kito atžvilgiu ir užtikrina, kad jie būtų nedelsiant perkelti į pagrindinę atmintį, jis vis tiek leidžia perskirstyti nepastovių kintamųjų skaitymus ir rašymą nepastovių skaitinių ir rašymo atžvilgiu. Tai reiškia - nebent visi Ištekliai laukai yra nepastovus taip pat - gija B vis tiek gali suvokti konstruktoriaus efektą kaip įvykusį po to išteklių yra nustatytas kaip nuoroda į naujai sukurtą Ištekliai.

DCL alternatyvos

Veiksmingiausias būdas išspręsti DCL idiomą yra jos išvengti. Paprasčiausias būdas to išvengti, žinoma, yra sinchronizavimas. Kai kintamąjį, parašytą viena gija, skaito kitas, turėtumėte naudoti sinchronizavimą, kad garantuotumėte, jog modifikacijos yra matomos kitoms gijoms nuspėjamai.

Kitas būdas išvengti problemų, susijusių su DCL, yra atsisakyti tingaus inicijavimo ir naudoti nekantrus inicijavimas. Užuot vilkinęs inicijuoti išteklių kol jis bus panaudotas, pradėkite jį tiesdami. Klasės krautuvas, kuris sinchronizuojamas klasėse ' Klasė objektas, vykdo statinius inicializavimo blokus klasės inicializavimo metu. Tai reiškia, kad statinių inicializatorių poveikis automatiškai matomas visoms gijoms, kai tik klasė įkeliama.

$config[zx-auto] not found$config[zx-overlay] not found