Az atomműveletek megjelenésének előfeltételei

Nézzük meg ezt a példát, hogy segítsen megérteni, hogyan működnek az atomi műveletek:

public class Counter {
    int count;

    public void increment() {
        count++;
    }
}

Ha egy szálunk van, minden remekül működik, de ha többszálat adunk hozzá, akkor rossz eredményt kapunk, és mindez azért, mert a növelési művelet nem egy művelet, hanem három: egy kérés az aktuális érték lekérésére.számol, majd növelje 1-gyel, és írjon újra ideszámol.

És ha két szál növelni akar egy változót, akkor nagy valószínűséggel adatvesztésre kerül sor. Vagyis mindkét szál 100-at kap, ennek eredményeként mindkettő 101-et ír a 102-es várható érték helyett.

És hogyan lehet megoldani? Zárakat kell használni. A szinkronizált kulcsszó segít megoldani ezt a problémát, használata garantálja, hogy egy szál egyszerre éri el a metódust.

public class SynchronizedCounterWithLock {
    private volatile int count;

    public synchronized void increment() {
        count++;
    }
}

Ezenkívül hozzá kell adnia a volatile kulcsszót , amely biztosítja a hivatkozások megfelelő láthatóságát a szálak között. Munkásságát fentebb áttekintettük.

De még mindig vannak árnyoldalai. A legnagyobb a teljesítmény, amikor sok szál próbál zárolást szerezni, és az egyik írási lehetőséget kap, a többi szál vagy blokkolva lesz, vagy felfüggesztve lesz, amíg a szál fel nem szabadul.

Mindezek a folyamatok, a blokkolások, a másik állapotba váltás nagyon költséges a rendszer teljesítménye szempontjából.

Atomműveletek

Az algoritmus alacsony szintű gépi utasításokat használ, mint például az összehasonlítás és csere (CAS, összehasonlítás és csere, amely biztosítja az adatok integritását, és már nagy mennyiségű kutatás folyik ezekről).

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

  • Memória a munkához (M)
  • Egy változó meglévő várható értéke (A).
  • Új érték (B) beállítandó

A CAS atomikusan frissíti M-et B-re, de csak akkor, ha M értéke megegyezik A-val, különben nem történik semmi.

Az első és a második esetben az M értéke kerül visszaadásra, ami lehetővé teszi három lépés kombinálását, nevezetesen az érték lekérését, az érték összehasonlítását és frissítését. És mindez egyetlen műveletté válik a gép szintjén.

Abban a pillanatban, amikor egy többszálú alkalmazás hozzáfér egy változóhoz, és megpróbálja frissíteni, és a CAS alkalmazásra kerül, akkor az egyik szál megkapja és frissíteni tudja. A zárolásokkal ellentétben azonban más szálak egyszerűen hibaüzenetet kapnak arról, hogy nem tudják frissíteni az értéket. Ezután áttérnek a további munkára, és a váltás teljesen kizárt az ilyen típusú munkáknál.

Ebben az esetben a logika megnehezül amiatt, hogy kezelnünk kell azt a helyzetet, amikor a CAS művelet nem működött sikeresen. Csak úgy modellezzük a kódot, hogy ne haladjon tovább, amíg a művelet nem sikerül.

Bevezetés az atomtípusokba

Találkoztál már olyan helyzettel, amikor be kell állítani a szinkronizálást a legegyszerűbb int típusú változóhoz ?

Az első módszer, amelyet már tárgyaltunk, a volatile + synchronized használata . De vannak speciális Atomic* órák is.

Ha CAS-t használunk, akkor a műveletek gyorsabban működnek az első módszerhez képest. Ezen kívül speciális és nagyon kényelmes módszereink vannak az érték hozzáadására, valamint a növelési és csökkentésére.

Az AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray olyan osztályok, amelyekben a műveletek atomi jellegűek. Az alábbiakban elemezzük a velük végzett munkát.

AtomicInteger

Az AtomicInteger osztály műveleteket biztosít egy int értékkel , amely atomian olvasható és írható, a kiterjesztett atomi műveletek mellett.

Olyan beszerzési és beállítási módszereket tartalmaz , amelyek úgy működnek, mint a változók olvasása és írása.

Vagyis „előtte történik” ugyanazon változó bármely későbbi beérkezésével, amelyről korábban beszéltünk. Az atomiccompleteAndSet metódus is rendelkezik ezekkel a memóriakonzisztencia jellemzőkkel.

Minden olyan művelet, amely új értéket ad vissza, atomosan történik:

int addAndGet (int delta) Adott értéket ad az aktuális értékhez.
logikai összehasonlításAndSet(elvárt int, frissítés int) Az értéket a megadott frissített értékre állítja be, ha az aktuális érték megegyezik a várt értékkel.
int decrementAndGet() Eggyel csökkenti az aktuális értéket.
int getAndAdd(int delta) A megadott értéket hozzáadja az aktuális értékhez.
int getAndDecrement() Eggyel csökkenti az aktuális értéket.
int getAndIncrement() Az aktuális értéket eggyel növeli.
int getAndSet(int newValue) Beállítja a megadott értéket, és visszaadja a régi értéket.
int incrementAndGet() Az aktuális értéket eggyel növeli.
lazySet(int newValue) Végül állítsa be a megadott értéket.
logikai gyengeCompareAndSet(várható, frissítés int) Az értéket a megadott frissített értékre állítja be, ha az aktuális érték megegyezik a várt értékkel.

Példa:

ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);

System.out.println(atomicInteger.get()); // prints 50