Vuodesta arvostettu Java-tyylin elementit sivulle JavaWorld (katso Java Tip 67), monet hyvää tarkoittavat Java-gurut kannustavat käyttämään kaksinkertaisesti tarkistettua lukitusta (DCL). Siinä on vain yksi ongelma - tämä älykäs näennäinen idiomi ei välttämättä toimi.
Kaksinkertaisesti tarkistettu lukitus voi olla vaarallista koodillesi!
Tämä viikko JavaWorld keskittyy kaksinkertaisesti tarkistetun lukitusidion vaaroihin. Lue lisää siitä, kuinka tämä näennäisesti vaaraton pikakuvake voi tuhota koodisi:- "Varoitus! Langoittaminen moniprosessorimaailmassa", Allen Holub
- Kaksinkertaisesti tarkistettu lukitus: älykäs, mutta rikki ", Brian Goetz
- Jos haluat puhua kaksinkertaisesti tarkistetusta lukitsemisesta, mene Allen Holubille Ohjelmointiteoria ja käytäntö -keskustelu
Mikä on DCL?
DCL-idiomi on suunniteltu tukemaan laiskaa alustusta, joka tapahtuu, kun luokka lykkää omistetun objektin alustamista, kunnes sitä todella tarvitaan:
luokka SomeClass {yksityisten resurssien resurssi = null; julkinen resurssi getResource () {if (resurssi == null) resurssi = uusi resurssi (); palautusresurssi; }}
Miksi haluat lykätä alustamista? Ehkä luoda Resurssi
on kallis toimenpide, ja JotkutLuokka
ei välttämättä soita getResource ()
missä tahansa ajon aikana. Siinä tapauksessa voit välttää Resurssi
täysin. Riippumatta JotkutLuokka
objekti voidaan luoda nopeammin, jos sen ei tarvitse luoda myös Resurssi
rakennusajankohtana. Joidenkin alustustoimintojen viivästyminen, kunnes käyttäjä todella tarvitsee tuloksia, voi auttaa ohjelmia käynnistymään nopeammin.
Entä jos yrität käyttää JotkutLuokka
monisäikeisessä sovelluksessa? Sitten saadaan kilpailuehtoja: kaksi ketjua voi suorittaa testin samanaikaisesti nähdäksesi resurssi
on nolla ja sen seurauksena alustaa resurssi
kahdesti. Sinun tulisi julistaa monisäikeisessä ympäristössä getResource ()
olla synkronoitu
.
Valitettavasti synkronoidut menetelmät toimivat paljon hitaammin - jopa 100 kertaa hitaammin - kuin tavalliset synkronoimattomat menetelmät. Yksi motiiveista laiskalle alustukselle on tehokkuus, mutta näyttää siltä, että nopeamman ohjelman käynnistymisen saavuttamiseksi sinun on hyväksyttävä hitaampi suoritusaika, kun ohjelma käynnistyy. Se ei kuulosta suurelta kompromissilta.
DCL pyrkii antamaan meille molempien maailmojen parhaat puolet. DCL: ää käyttämällä getResource ()
menetelmä näyttäisi tältä:
luokka SomeClass {yksityisten resurssien resurssi = null; julkinen resurssi getResource () {if (resurssi == null) {synkronoitu {if (resurssi == null) resurssi = uusi resurssi (); }} palautusresurssi; }}
Ensimmäisen puhelun jälkeen getResource ()
, resurssi
on jo alustettu, mikä välttää synkronointihittin yleisimmällä koodipolulla. DCL estää myös kilpailutilanteen tarkistamalla resurssi
toisen kerran synkronoidun lohkon sisällä; varmistaa, että vain yksi ketju yrittää alustaa resurssi
. DCL näyttää fiksulta optimoinnilta - mutta se ei toimi.
Tapaa Java-muistimalli
Tarkemmin sanottuna DCL: n ei voida taata toimivan. Ymmärtääksemme miksi meidän on tarkasteltava JVM: n ja tietokoneympäristön suhdetta, jossa se toimii. Erityisesti meidän on tarkasteltava Java-muistimallia (JMM), joka on määritelty Java-kielimääritys, kirjoittanut Bill Joy, Guy Steele, James Gosling ja Gilad Bracha (Addison-Wesley, 2000), jossa kerrotaan, miten Java käsittelee langan ja muistin välistä vuorovaikutusta.
Toisin kuin useimmat muut kielet, Java määrittelee suhteensa taustalla olevaan laitteistoon muodollisen muistimallin kautta, jonka odotetaan olevan kaikilla Java-alustoilla. Vertailun vuoksi muilta kieliltä, kuten C ja C ++, puuttuu muodollinen muistimalli; tällaisilla kielillä ohjelmat perivät sen laitteistoalustan muistimallin, jolla ohjelma toimii.
Kun suoritetaan synkronisessa (yksisäikeisessä) ympäristössä, ohjelman vuorovaikutus muistin kanssa on melko yksinkertaista tai ainakin näyttää siltä. Ohjelmat tallentavat kohteita muistipaikkoihin ja odottavat, että ne ovat edelleen siellä seuraavalla kerralla, kun muistipaikkoja tutkitaan.
Itse asiassa totuus on aivan erilainen, mutta kääntäjän, JVM: n ja laitteiston ylläpitämä monimutkainen harha piilottaa sen meiltä. Vaikka ajattelemme ohjelmien suorittavan peräkkäin - ohjelmakoodin määrittelemässä järjestyksessä - sitä ei aina tapahdu. Kääntäjät, prosessorit ja välimuistit voivat vapaasti käyttää kaikenlaisia vapauksia ohjelmiemme ja tietojemme kanssa, kunhan ne eivät vaikuta laskennan tulokseen. Esimerkiksi kääntäjät voivat luoda käskyjä eri järjestyksessä kuin ohjelman ilmeinen tulkinta ja tallentaa muuttujat rekistereihin muistin sijaan; prosessorit voivat suorittaa ohjeita rinnakkain tai epäkunnossa; ja välimuistit voivat vaihdella järjestyksessä, jossa kirjoitukset sitoutuvat päämuistiin. JMM: n mukaan kaikki nämä erilaiset uudelleenjärjestelyt ja optimoinnit ovat hyväksyttäviä, kunhan ympäristö säilyy ikään kuin sarjana semantiikka - ts. niin kauan kuin saavutat saman tuloksen kuin sinulla olisi, jos ohjeet suoritettaisiin tiukasti peräkkäisessä ympäristössä.
Kääntäjät, prosessorit ja välimuistit järjestävät ohjelmatoimintojen järjestyksen paremman suorituskyvyn saavuttamiseksi. Viime vuosina olemme nähneet valtavia parannuksia laskentatehokkuudessa. Suuremmat prosessorin kellotaajuudet ovat vaikuttaneet merkittävästi parempaan suorituskykyyn, mutta lisääntynyt rinnakkaisuus (putkilinjaisten ja superskalaaristen suoritusyksiköiden, dynaamisen käskyjen ajoituksen ja spekulatiivisen suorituksen sekä kehittyneiden monitasoisten muistivälimuistojen muodossa) on myös ollut merkittävä tekijä. Samalla kääntäjien kirjoittaminen on kasvanut paljon monimutkaisemmaksi, koska kääntäjän on suojattava ohjelmoijaa näiltä monimutkaisuuksilta.
Kun kirjoitat yksisäikeisiä ohjelmia, et näe näiden eri ohjeiden tai muistitoimintojen uudelleenjärjestelyjen vaikutuksia. Monisäikeisissä ohjelmissa tilanne on kuitenkin aivan erilainen - yksi säie voi lukea toisen säikeen kirjoittamat muistipaikat. Jos säie A muokkaa joitain muuttujia tietyssä järjestyksessä, synkronoinnin puuttuessa säie B ei ehkä näe niitä samassa järjestyksessä - tai ei ehkä näe niitä lainkaan. Tämä voi johtua siitä, että kääntäjä järjestää ohjeet uudelleen tai tallensi muuttujan väliaikaisesti rekisteriin ja kirjoitti sen muistiin myöhemmin; tai koska prosessori suoritti ohjeet rinnakkain tai eri järjestyksessä kuin määritetty kääntäjä; tai koska ohjeet olivat eri muistialueilla, ja välimuisti päivitti vastaavat päämuistipaikat eri järjestyksessä kuin siinä, johon ne kirjoitettiin. Olosuhteista riippumatta monisäikeiset ohjelmat ovat luonnostaan vähemmän ennustettavia, ellet nimenomaisesti varmista, että langoilla on yhtenäinen näkymä muistista synkronoinnin avulla.
Mitä synkronoitu todella tarkoittaa?
Java kohtelee kutakin ketjua ikään kuin se toimisi omalla prosessorillaan, jolla on oma paikallinen muisti, joista kukin puhuu ja synkronoi jaetun päämuistin kanssa. Jopa yhden prosessorin järjestelmässä kyseinen malli on järkevä muistivälimuistien vaikutusten ja prosessorirekisterien käytön vuoksi muuttujien tallentamiseksi. Kun säie muuttaa sijaintia paikallisessa muistissaan, tämän muutoksen pitäisi lopulta näkyä myös päämuistissa, ja JMM määrittelee säännöt, milloin JVM: n on siirrettävä tietoja paikallisen ja päämuistin välillä. Java-arkkitehdit tajusivat, että liian rajoittava muistimalli heikentäisi vakavasti ohjelman suorituskykyä. He yrittivät luoda muistimallin, joka antaisi ohjelmille mahdollisuuden toimia hyvin nykyaikaisella tietokonelaitteistolla tarjoten silti takuita, jotka antaisivat ketjujen olla vuorovaikutuksessa ennustettavalla tavalla.
Java on ensisijainen työkalu vuorovaikutusten hahmottamiseksi ketjujen välillä ennustettavasti synkronoitu
avainsana. Monet ohjelmoijat ajattelevat synkronoitu
tiukasti keskinäisen syrjäytymisen semaforin (mutex) estää kriittisten osien suorittamisen useammalla kuin yhdellä säikeellä kerrallaan. Valitettavasti tämä intuitio ei kuvaa täysin mitä synkronoitu
tarkoittaa.
Semantiikka synkronoitu
eivät todellakaan sisällä keskinäistä poissulkemista suorituksesta semaforin tilan perusteella, mutta ne sisältävät myös sääntöjä synkronointilangan vuorovaikutuksesta päämuistin kanssa. Erityisesti lukon hankinta tai vapauttaminen laukaisee a muistimuuri - pakollinen synkronointi langan paikallisen muistin ja päämuistin välillä. (Joillakin prosessoreilla - kuten Alpha - on selkeät koneohjeet muistiestojen suorittamiseksi.) Kun säie poistuu a synkronoitu
Lohko, se suorittaa kirjoitusesteen - sen on huuhdeltava kaikki lohkossa muokatut muuttujat päämuistiin ennen lukituksen vapauttamista. Vastaavasti, kun syötät a synkronoitu
lohko, se suorittaa lukunesteen - se on kuin paikallinen muisti olisi mitätöity, ja sen on haettava kaikki muuttujat, joihin lohkossa viitataan, päämuistista.
Synkronoinnin asianmukainen käyttö takaa, että yksi säie näkee toisen vaikutukset ennustettavalla tavalla. Vasta kun ketjut A ja B synkronoituvat samaan objektiin, JMM takaa, että säie B näkee säikeen A tekemät muutokset ja että säikeen A tekemät muutokset synkronoitu
esto ilmestyy atomisesti ketjuun B (joko koko lohko suoritetaan tai mikään niistä ei.) Lisäksi JMM varmistaa sen synkronoitu
lohkot, jotka synkronoivat samalla objektilla, näyttävät suoritettavan samassa järjestyksessä kuin ohjelmassa.
Joten mikä on rikki DCL: ssä?
DCL perustuu .synkronoimattomaan käyttöön resurssi
ala. Se näyttää olevan vaaraton, mutta ei ole. Kuvittele, että lanka A on sisällä synkronoitu
lohko, suorittamalla käsky resurssi = uusi resurssi ();
kun lanka B on juuri tulossa getResource ()
. Harkitse tämän alustuksen vaikutusta muistiin. Muisti uudelle Resurssi
kohde varataan; rakentaja Resurssi
kutsutaan, alustamalla uuden objektin jäsenkentät; ja kenttä resurssi
/ JotkutLuokka
määritetään viittaus äskettäin luotuun objektiin.
Koska lanka B ei kuitenkaan suorita a: n sisällä synkronoitu
lohko, se voi nähdä nämä muistitoiminnot eri järjestyksessä kuin yksi säie A suorittaa. Saattaa olla, että B näkee nämä tapahtumat seuraavassa järjestyksessä (ja kääntäjä voi myös vapaasti järjestää uudelleen tällaiset ohjeet): allokoi muisti, anna viite resurssi
, puhelun rakentaja. Oletetaan, että lanka B tulee mukaan, kun muisti on varattu ja resurssi
kenttä on asetettu, mutta ennen kuin konstruktoria kutsutaan. Se näkee sen resurssi
ei ole nolla, ohittaa synkronoitu
lohko ja palauttaa viitteen osittain rakennettuun Resurssi
! Tarpeetonta sanoa, että tulos ei ole odotettavissa eikä toivottu.
Kun tämä esimerkki esitetään, monet ihmiset ovat aluksi skeptisiä. Monet erittäin älykkäät ohjelmoijat ovat yrittäneet korjata DCL: n niin, että se toimii, mutta mikään näistä oletettavasti kiinteistä versioista ei myöskään toimi. On huomattava, että DCL saattaa itse asiassa toimia joidenkin JVM-mallien versioissa - koska harvat JVM: t todella toteuttavat JMM: n oikein. Et kuitenkaan halua, että ohjelmiesi oikeellisuus luottaa toteutuksen yksityiskohtiin - etenkin virheisiin -, jotka liittyvät nimenomaan käyttämäsi JVM: n tiettyyn versioon.
Muut samanaikaisuusvaarat on upotettu DCL: ään - ja kaikkiin synkronoimattomiin viitteisiin muistiin, jonka toinen säike on kirjoittanut, jopa harmittomalta näyttävältä. Oletetaan, että lanka A on alustanut Resurssi
ja poistuu synkronoitu
estää, kun lanka B tulee sisään getResource ()
. Nyt Resurssi
on täysin alustettu ja lanka A huuhdo paikallisen muistinsa päämuistiin. resurssi
Kentät voivat viitata muihin muistiin tallennettuihin kohteisiin jäsenkenttien kautta, jotka myös tyhjennetään. Vaikka ketju B saattaa nähdä kelvollisen viittauksen uuteen luotuun Resurssi
, koska se ei suorittanut lukunestettä, se näki silti arvot resurssi
jäsenen kentät.
Haihtuva ei tarkoita sitä, mitä ajattelet
Yleisesti ehdotettu korjaamaton on ilmoittaa resurssi
kenttä JotkutLuokka
kuten haihtuva
. Vaikka JMM estää haihtuvien muuttujien kirjoitusten järjestämisen uudelleen toistensa suhteen ja varmistaa, että ne huuhdellaan päämuistiin välittömästi, se sallii kuitenkin haihtuvien muuttujien lukemisen ja kirjoittamisen järjestyksen uudelleen haihtumattomien lukujen ja kirjojen suhteen. Se tarkoittaa - ellei kaikkia Resurssi
kentät ovat haihtuva
samoin - lanka B voi silti havaita konstruktorin vaikutuksen tapahtuvan sen jälkeen resurssi
on asetettu viittaamaan äskettäin luotuun Resurssi
.
Vaihtoehdot DCL: lle
Tehokkain tapa korjata DCL-idioomi on välttää se. Yksinkertaisin tapa välttää se on tietysti synkronointi. Aina, kun toinen lukee toisen säikeen kirjoittaman muuttujan, sinun on käytettävä synkronointia varmistaaksesi, että muutokset näkyvät muille säikeille ennustettavalla tavalla.
Toinen vaihtoehto DCL-ongelmien välttämiseksi on pudottaa laiska alustus ja käyttää sen sijaan innokas alustus. Sen sijaan, että viivästyttäisi resurssi
alusta se rakentamisen aikana, kunnes sitä käytetään ensimmäisen kerran. Luokkakuormaaja, joka synkronoi luokkien Luokka
objekti, suorittaa staattiset alustuslohkot luokan alustushetkellä. Tämä tarkoittaa, että staattisten alustusohjelmien vaikutus näkyy automaattisesti kaikille säikeille heti, kun luokka latautuu.