Ohjelmointi

Vältä synkronointilukoksia

Aikaisemmassa artikkelissani "Double Checked Locking: Clever, but Broken" (JavaWorld, Helmikuu 2001) kuvasin, kuinka monet yleiset tekniikat synkronoinnin välttämiseksi ovat itse asiassa vaarallisia, ja suosittelin strategiaa "Jos olet epävarma, synkronoi". Yleensä sinun on synkronoitava aina, kun luet muuttujaa, joka on ehkä aiemmin kirjoitettu eri säikeellä, tai aina, kun kirjoitat muuttujaa, jonka toinen ketju voi lukea myöhemmin. Vaikka synkronoinnilla on suoritusrangaistus, tahattomaan synkronointiin liittyvä rangaistus ei ole yhtä suuri kuin jotkut lähteet ovat ehdottaneet, ja se on vähentynyt tasaisesti jokaisen peräkkäisen JVM-toteutuksen kanssa. Joten näyttää siltä, ​​että nyt on vähemmän syytä kuin koskaan välttää synkronointia. Liialliseen synkronointiin liittyy kuitenkin toinen riski: umpikuja.

Mikä on umpikuja?

Sanomme, että joukko prosesseja tai ketjuja on umpikujassa kun jokainen ketju odottaa tapahtumaa, jonka vain toinen prosessi sarjassa voi aiheuttaa. Toinen tapa havainnollistaa umpikujaa on rakentaa suunnattu graafi, jonka pisteet ovat säikeitä tai prosesseja ja jonka reunat edustavat "odottaa-yhteyttä" -suhdetta. Jos tämä kaavio sisältää jakson, järjestelmä on umpikujassa. Ellei järjestelmää ole suunniteltu toipumaan umpikujasta, umpikuja aiheuttaa ohjelman tai järjestelmän jumittumisen.

Synkronointilukot Java-ohjelmissa

Pysähdyksiä voi esiintyä Java-sovelluksessa, koska synkronoitu avainsana saa suoritettavan säikeen estymään odottaessaan määritettyyn objektiin liittyvää lukitusta tai näyttöä. Koska lanka saattaa jo pitää lukkoja, jotka liittyvät muihin esineisiin, kaksi ketjua voisi kukin odottaa toisten vapauttavan lukon; tällöin he odottavat odottavan ikuisesti. Seuraava esimerkki näyttää joukon menetelmiä, joilla on mahdollisuus umpikujaan. Molemmat menetelmät hankkivat lukot kahteen lukitusobjektiin, cacheLock ja tableLock, ennen kuin ne jatkavat. Tässä esimerkissä lukkoina toimivat objektit ovat globaaleja (staattisia) muuttujia, yleinen tekniikka sovellusten lukituksen yksinkertaistamiseksi suorittamalla lukitus karkeammalla tarkkuustasolla:

Luettelo 1. Mahdollinen synkronoinnin umpikuja

 public staattinen Object cacheLock = new Object (); public staattinen Object tableLock = uusi objekti (); ... public void oneMethod () {synchronized (cacheLock) {synchronized (tableLock) {doSomething (); }}} public void anotherMethod () {synchronized (tableLock) {synchronized (cacheLock) {doSomethingElse (); }}} 

Kuvittele nyt, että säie A kutsuu oneMethod () samalla kun lanka B kutsuu samanaikaisesti anotherMethod (). Kuvittele edelleen, että lanka A saa lukon cacheLockja samanaikaisesti lanka B saa lukituksen tableLock. Nyt langat ovat lukkiutuneet: kumpikaan lanka ei luovuta lukkoaan ennen kuin se saa toisen lukon, mutta kumpikaan lanka ei voi hankkia toista lukkoa ennen kuin toinen lanka luovuttaa sen. Kun Java-ohjelma lukkiutuu, umpikujaan johtavat säikeet odottavat vain ikuisesti. Vaikka muut ketjut saattavat jatkua, joudut lopulta tappamaan ohjelman, käynnistämään sen uudelleen ja toivomaan, että se ei enää ole umpikujassa.

Salpalukituksen testaaminen on vaikeaa, koska umpikuja riippuu ajoituksesta, kuormituksesta ja ympäristöstä, ja näin ollen se voi tapahtua harvoin tai vain tietyissä olosuhteissa. Koodilla voi olla umpikuja, kuten Listing 1, mutta se ei näytä umpikujaa, ennen kuin tapahtuu satunnaisten ja satunnaisten tapahtumien yhdistelmiä, kuten ohjelmalle, joka on tietyn kuormituksen alainen, ajettu tietyllä laitteistokokoonpanolla tai altistettu tietylle yhdistelmä käyttäjän toimia ja ympäristöolosuhteita. Salpalukot muistuttavat aikapommeja, jotka odottavat räjähtämistä koodissamme; kun he tekevät, ohjelmamme yksinkertaisesti roikkuvat.

Epäyhtenäinen lukitusjärjestys aiheuttaa umpikujaan

Onneksi voimme asettaa lukkojen hankinnalle suhteellisen yksinkertaisen vaatimuksen, joka voi estää synkronoinnin umpikujat. Luettelon 1 menetelmillä on mahdollisuus umpikujaan, koska kukin menetelmä hankkii kaksi lukkoa eri järjestyksessä. Jos luettelo 1 olisi kirjoitettu siten, että kukin menetelmä hankki kaksi lukkoa samassa järjestyksessä, kaksi tai useampi näitä menetelmiä suorittava ketju ei voinut umpikujaan ajoituksesta tai muista ulkoisista tekijöistä huolimatta, koska mikään säie ei voinut hankkia toista lukkoa pitämättä jo ensimmäinen. Jos voit taata, että lukot hankitaan aina yhtenäisessä järjestyksessä, ohjelma ei ole umpikujassa.

Pysähdykset eivät aina ole niin ilmeisiä

Kun olet lukenut lukkojen tilaamisen tärkeyden, voit helposti tunnistaa Listing 1: n ongelman. Vastaavat ongelmat saattavat kuitenkin osoittautua vähemmän ilmeisiksi: ehkä nämä kaksi menetelmää sijaitsevat erillisissä luokissa, tai kenties mukana olevat lukot hankitaan implisiittisesti kutsumalla synkronoituja menetelmiä eikä nimenomaisesti synkronoidun lohkon kautta. Harkitse näitä kahta yhteistyössä toimivaa luokkaa, Malli ja Näytä, yksinkertaistetussa MVC (Model-View-Controller) -kehyksessä:

Listaus 2. Hienovaraisempi mahdollinen synkronoinnin umpikuja

 julkisen luokan malli {yksityinen Näytä myView; julkinen synkronoitu void updateModel (Object someArg) {doSomething (someArg); myView.somethingChanged (); } julkinen synkronoitu objekti getSomething () {return someMethod (); }} public class Näytä {private Model aluseksModel; julkinen synkronoitu void somethingChanged () {doSomething (); } julkinen synkronoitu void updateView () {Object o = myModel.getSomething (); }} 

Listalla 2 on kaksi yhteistyössä toimivaa objektia, joilla on synkronoituja menetelmiä; kukin objekti kutsuu toisen synkronoidut menetelmät. Tämä tilanne muistuttaa luetteloa 1 - kaksi menetelmää hankkivat lukot samoille kahdelle objektille, mutta eri järjestyksessä. Epäyhtenäinen lukituksen järjestys tässä esimerkissä on kuitenkin paljon vähemmän ilmeinen kuin luettelossa 1, koska lukon hankinta on implisiittinen osa menetelmäpuhelua. Jos yksi ketju soittaa Model.updateModel () samalla kun toinen säie kutsuu samanaikaisesti View.updateView (), ensimmäinen säie voisi saada Mallilukitsee ja odota Näytälukko, kun taas toinen saa Näytälukitsee ja odottaa ikuisesti Mallilukko.

Voit hautaa synkronoinnin umpikujan mahdollisuudet vielä syvemmälle. Harkitse tätä esimerkkiä: Sinulla on tapa siirtää varoja tililtä toiselle. Haluat hankkia lukot molemmille tileille ennen siirron suorittamista varmistaaksesi, että siirto on atominen. Harkitse tätä vaarattomalta näyttävää toteutusta:

Listaus 3. Vielä hienovaraisempi mahdollinen synkronoinnin umpikuja

 public void transferMoney (Account fromAccount, Account toAccount, DollarAmount amountToTransfer) {synkronoitu (fromAccount) {synchronized (toAccount) {if (fromAccount.hasSufficientBalance (summaToTransfer) {fromAccount.debit (summaToTransfer}); siirto (siirto); } 

Vaikka kaikki kahdella tai useammalla tilillä toimivat menetelmät käyttävät samaa järjestystä, luettelo 3 sisältää saman umpikujan ongelman siemenet kuin luettelot 1 ja 2, mutta tasaisemmalla tavalla. Mieti, mitä tapahtuu, kun säie A suorittaa:

 transferMoney (accountOne, accountTwo, summa); 

Samanaikaisesti lanka B suorittaa:

 transferMoney (tiliTwo, accountOne, anotherAmount); 

Jälleen nämä kaksi säiettä yrittävät hankkia samat kaksi lukkoa, mutta eri järjestyksessä; umpikujasta aiheutuva riski uhkaa edelleen, mutta paljon vähemmän ilmeisessä muodossa.

Kuinka välttää umpikujat

Yksi parhaista tavoista estää umpikuja on välttää useamman kuin yhden lukon hankkiminen kerrallaan, mikä on usein käytännöllistä. Jos se ei kuitenkaan ole mahdollista, tarvitset strategian, joka varmistaa, että hankit useita lukkoja yhtenäisessä, määritellyssä järjestyksessä.

Riippuen siitä, miten ohjelmasi käyttää lukituksia, ei välttämättä ole monimutkaista varmistaa, että käytät yhtenäistä lukitusjärjestystä. Joissakin ohjelmissa, kuten luettelossa 1, kaikki kriittiset lukot, jotka saattavat osallistua monilukitukseen, vedetään pienestä joukosta yksittäisiä lukitusobjekteja. Siinä tapauksessa voit määrittää lukkojen hankintajärjestyksen lukkojoukkoon ja varmistaa, että hankit aina lukot siinä järjestyksessä. Kun lukitusjärjestys on määritelty, se on yksinkertaisesti dokumentoitava hyvin kannustamaan yhdenmukaista käyttöä koko ohjelmassa.

Kutista synkronoituja lohkoja lukituksen välttämiseksi

Listalla 2 ongelma kasvaa monimutkaisemmaksi, koska synkronoidun menetelmän kutsumisen seurauksena lukot hankitaan implisiittisesti. Voit yleensä välttää sellaiset mahdolliset umpikujat, jotka johtuvat tapauksista, kuten Lista 2, kaventamalla synkronoinnin laajuutta mahdollisimman pieneksi lohkoksi. Tekee Model.updateModel () todella pitää pitää Malli lukitse, kun se soittaa View.somethingChanged ()? Usein ei; koko menetelmä synkronoitiin todennäköisesti pikakuvakkeena pikemminkin kuin siksi, että koko menetelmä oli synkronoitava. Jos kuitenkin korvataan synkronoidut menetelmät pienemmillä synkronoiduilla lohkoilla menetelmän sisällä, sinun on dokumentoitava tämä lukitustapa osana menetelmän Javadocia. Soittajien on tiedettävä, että he voivat soittaa menetelmään turvallisesti ilman ulkoista synkronointia. Soittajien tulisi myös tietää menetelmän lukituskäyttäytyminen, jotta he voivat varmistaa, että lukot hankitaan yhtenäisessä järjestyksessä.

Kehittyneempi lukitustilaustekniikka

Muissa tilanteissa, kuten Listing 3: n pankkitiliesimerkissä, kiinteämääräisen säännön soveltaminen kasvaa vielä monimutkaisemmaksi; sinun on määritettävä lukittavien kohteiden kokonaisjärjestys ja käytettävä tätä järjestystä lukituksen hankkimisen järjestyksen valitsemiseksi. Tämä kuulostaa sotkuiselta, mutta on itse asiassa suoraviivaista. Listaus 4 kuvaa tätä tekniikkaa; se käyttää numeerista tilinumeroa saadakseen tilauksen Tili esineitä. (Jos lukittavasta objektista puuttuu luonnollinen identiteettiominaisuus, kuten tilinumero, voit käyttää Object.identityHashCode () menetelmä sen sijaan.)

Listaus 4. Käytä tilausta lukkojen hankkimiseen kiinteässä järjestyksessä

 public void transferMoney (tili tililtä, ​​tili tilille, dollari määrä summansiirto) {tili firstLock, secondLock; jos (fromAccount.accountNumber () == toAccount.accountNumber ()) heittää uuden poikkeuksen ("Ei voi siirtää tililtä itselle"); else if (fromAccount.accountNumber () <toCount.accountNumber ()) {firstLock = fromAccount; secondLock = tilille; } else {firstLock = tilille; secondLock = tililtä; } synkronoitu (firstLock) {synchronized (secondLock) {if (fromAccount.hasSufficientBalance (amountToTransfer) {fromAccount.debit (summaToTransfer); toAccount.credit (summaToTransfer);}}}} 

Nyt järjestys, jossa tilit määritetään puhelussa siirtää rahaa() ei ole väliä; lukot hankitaan aina samassa järjestyksessä.

Tärkein osa: Dokumentaatio

Kriittinen - mutta usein unohdettu - elementti lukitusstrategiassa on dokumentointi. Valitettavasti jopa dokumentoinnissa käytetään paljon vähemmän vaivaa, vaikka lukitusstrategian suunnittelussa kiinnitettäisiin paljon huomiota. Jos ohjelmassasi on pieni joukko yksittäisiä lukkoja, sinun on dokumentoitava lukituksen järjestämistä koskevat oletuksesi mahdollisimman selkeästi, jotta tulevat ylläpitäjät voivat täyttää lukituksen järjestysvaatimukset. Jos menetelmän on hankittava lukko tehtävänsä suorittamiseksi tai sitä on kutsuttava pitämällä erityistä lukkoa, menetelmän Javadocin tulisi huomioida tämä tosiasia. Näin tulevat kehittäjät tietävät, että tietyn menetelmän kutsuminen voi edellyttää lukon hankkimista.

Harvat ohjelmat tai luokkakirjastot dokumentoivat lukituksen käytön asianmukaisesti. Jokaisen menetelmän tulisi ainakin dokumentoida saamansa lukot ja pitääkö soittajien pitää olla lukko, jotta menetelmä voidaan kutsua turvallisesti. Lisäksi luokkien on dokumentoitava, ovatko ne langattomia vai eivät, tai missä olosuhteissa.

Keskity lukituskäyttäytymiseen suunnitteluhetkellä

Koska umpikujat eivät useinkaan ole ilmeisiä ja tapahtuvat harvoin ja arvaamattomasti, ne voivat aiheuttaa vakavia ongelmia Java-ohjelmissa. Kun kiinnität huomiota ohjelman lukituskäyttäytymiseen suunnitteluhetkellä ja määrität säännöt useiden lukkojen hankkimisajankohdasta ja tavasta, voit vähentää umpikujaiden todennäköisyyttä huomattavasti. Muista dokumentoida ohjelman lukituksen hankintasäännöt ja sen synkronoinnin käyttö huolellisesti; yksinkertaisten lukitusoletusten dokumentointiin käytetty aika maksaa itsensä hyväksi vähentämällä huomattavasti umpikujan ja muiden samanaikaisuusongelmien mahdollisuutta myöhemmin.

Brian Goetz on ammattimainen ohjelmistokehittäjä, jolla on yli 15 vuoden kokemus. Hän on pääkonsultti Quiotixissa, ohjelmistokehitys- ja konsulttiyrityksessä, joka sijaitsee Los Altosissa, Kaliforniassa.
$config[zx-auto] not found$config[zx-overlay] not found