Bevezetés az atomi változókba a Java-ban

1. Bemutatkozás

Egyszerűen fogalmazva, a közös mutábilis állapot nagyon könnyen vezet problémákhoz, ha párhuzamosságról van szó. Ha a megosztott, módosítható objektumokhoz való hozzáférést nem kezelik megfelelően, az alkalmazások gyorsan hajlamosak lehetnek néhány nehezen észlelhető párhuzamossági hibára.

Ebben a cikkben áttekintjük a zárak használatát az egyidejű hozzáférés kezelésére, feltárjuk a zárakkal kapcsolatos néhány hátrányt, és végül alternatívaként atomváltozókat vezetünk be.

2. Zárak

Vessünk egy pillantást az osztályra:

public class Counter {int számláló; public void increment () {számláló ++; }}

Egyszálas környezet esetén ez tökéletesen működik; amint azonban egynél több szál engedélyezését engedélyezzük, elkezdünk következetlen eredményeket elérni.

Ennek oka az egyszerű növekményes művelet (számláló ++), amely úgy nézhet ki, mint egy atomi művelet, de valójában három művelet kombinációja: az érték megszerzése, az inkrementálás és a frissített érték visszaírása.

Ha két szál egyszerre próbálja megszerezni és frissíteni az értéket, az elveszített frissítéseket eredményezhet.

Az objektumokhoz való hozzáférés kezelésének egyik módja a zárak használata. Ezt a szinkronizált kulcsszó a növekedés módszer aláírása. A szinkronizált A kulcsszó biztosítja, hogy egyszerre csak egy szál léphessen be a módszerbe (a zárolásról és a szinkronizálásról bővebben itt olvashat: Útmutató a Java szinkronizált kulcsszavához):

public class SafeCounterWithLock {private volatile int counter; public synchronized void increment () {számláló ++; }}

Ezenkívül hozzá kell adnunk a illó kulcsszó a szálak megfelelő hivatkozási láthatóságának biztosításához.

A zárak használata megoldja a problémát. Az előadás azonban nagy sikert arat.

Ha több szál megkísérli megszerezni a zárat, egyikük nyer, míg a többi szál blokkolva vagy felfüggesztve van.

A szál felfüggesztése, majd folytatása nagyon költséges és befolyásolja a rendszer általános hatékonyságát.

Egy kis programban, például a számláló, a kontextusváltásra fordított idő jóval több lehet, mint a tényleges kódfuttatás, ezáltal nagymértékben csökken az általános hatékonyság.

3. Atomműveletek

Van egy kutatási ág, amely nem blokkoló algoritmusok létrehozására összpontosít egyidejű környezetek számára. Ezek az algoritmusok alacsony szintű atomi gépi utasításokat használnak, mint például az összehasonlítás és cserélés (CAS) az adatok integritásának biztosítása érdekében.

Egy tipikus CAS-művelet három operanduson működik:

  1. A memória működési helye (M)
  2. A változó meglévő várható értéke (A)
  3. Az új (B) érték, amelyet be kell állítani

A CAS művelet az M-ben szereplő értéket atomilag frissíti B-re, de csak akkor, ha az M-ben lévő meglévő érték megegyezik A-val, ellenkező esetben semmilyen művelet nem történik.

Mindkét esetben az M-ben megadott értéket adjuk vissza. Ez három lépést - az érték megszerzése, az érték összehasonlítása és az érték frissítése - egyetlen gépszintű műveletté ötvözi.

Amikor több szál megpróbálja ugyanazt az értéket frissíteni a CAS-on keresztül, egyikük nyeri és frissíti az értéket. A zárakkal ellentétben azonban más szálat nem függesztenek fel; ehelyett egyszerűen tájékoztatják őket arról, hogy nem sikerült frissíteni az értéket. Ezután a szálak további munkát végezhetnek, és a kontextusváltást teljesen elkerülik.

Az egyik másik következmény az, hogy az alapvető programlogika bonyolultabbá válik. Ennek oka, hogy kezelnünk kell azt a forgatókönyvet, amikor a CAS-művelet nem sikerült. Újra és újra megpróbálhatjuk, amíg sikerül, vagy nem tehetünk semmit, és a használati esettől függően továbbmehetünk.

4. Atomváltozók a Java-ban

A Java leggyakrabban használt atomváltozó osztályai az AtomicInteger, AtomicLong, AtomicBoolean és AtomicReference. Ezek az osztályok egy int, hosszú, logikai érték, illetve objektum hivatkozás, amelyek atomszerűen frissíthetők. Az ezen osztályok által kitett fő módszerek a következők:

  • kap() - megkapja az értéket a memóriából, hogy a többi szál által végrehajtott változtatások láthatók legyenek; egyenértékű az olvasással a illó változó
  • készlet() - beírja az értéket a memóriába, hogy a változás más szálak számára is látható legyen; az írással egyenértékű a illó változó
  • lazySet () - végül beírja az értéket a memóriába, esetleg átrendezi a későbbi releváns memóriaműveletekkel. Az egyik felhasználási eset a hivatkozások érvénytelenítését jelenti a szemétszállítás érdekében, amelyhez soha többé nem lesz szükség. Ebben az esetben jobb teljesítményt érünk el a null késleltetésével illó ír
  • CompareAndSet () - a 3. szakaszban leírtakkal megegyezően igaz, ha sikerül, másként hamis
  • gyengeCompareAndSet () - ugyanaz, mint a 3. szakaszban leírtak, de gyengébb abban az értelemben, hogy nem hoz létre történéseket a megrendelések előtt. Ez azt jelenti, hogy nem feltétlenül látja más változók frissítéseit. A Java 9-től kezdve ez a módszer minden atomi megvalósításban elavult a javára gyengeCompareAndSetPlain (). A memória effektusai gyengeCompareAndSet () egyértelműek voltak, de a nevek volatilis memóriahatásokat sejtettek. Ennek a zavarnak az elkerülése érdekében elavulták ezt a módszert, és négy módszert adtak hozzá, különféle memóriaeffektusokkal, például gyengeCompareAndSetPlain () vagy gyengeCompareAndSetVolatile ()

Menettel biztonságos számlálóval megvalósítva AtomicInteger az alábbi példában látható:

public class SafeCounterWithoutLock {private final AtomicInteger counter = new AtomicInteger (0); public int getValue () {return counter.get (); } public void increment () {while (true) {int existingValue = getValue (); int újérték = meglévő érték + 1; if (számláló.összehasonlítAndSet (meglévőérték, újérték)) {return; }}}}

Amint láthatja, újra megpróbáljuk a CompareAndSet műveletet és megint a meghibásodást, mivel szeretnénk garantálni, hogy a növekedés módszer mindig 1-vel növeli az értéket.

5. Következtetés

Ebben a gyors bemutatóban a párhuzamosság kezelésének alternatív módját írtuk le, ahol a zárolással kapcsolatos hátrányok elkerülhetők. Megvizsgáltuk a Java atomi változó osztályainak kitett főbb módszereket is.

Mint mindig, a példák is elérhetők a GitHubon.

További, nem blokkoló algoritmusokat belsőleg használó osztályok felfedezéséhez olvassa el a ConcurrentMap útmutatóját.