Szoftver tranzakciós memória Java-ban a Multiverse használatával

1. Áttekintés

Ebben a cikkben megnézzük a Multiverzum könyvtár - amely segít a Szoftver tranzakciós memória Java-ban.

Ebből a könyvtárból származó konstrukciók segítségével létrehozhatunk egy szinkronizálási mechanizmust megosztott állapotban - ami elegánsabb és olvashatóbb megoldás, mint a Java központi könyvtárral történő standard megvalósítás.

2. Maven-függőség

A kezdéshez hozzá kell adnunk a multiverzum-mag könyvtár a pomunkba:

 org.multiverse multiverse-core 0.7.0 

3. Multiverse API

Kezdjük az alapokkal.

A szoftveres tranzakciós memória (STM) egy olyan fogalom, amely az SQL adatbázis világából származik - ahol minden műveletet olyan tranzakciókon belül hajtanak végre, amelyek kielégítik a ACID (atomosság, konzisztencia, izolálás, tartósság) tulajdonságait. Itt, csak az atomosság, a következetesség és az izolálás elégedett, mert a mechanizmus a memóriában fut.

A Multiverse könyvtár fő felülete a TxnObject - minden tranzakciós objektumnak meg kell valósítania azt, és a könyvtár számos speciális alosztályt biztosít számunkra, amelyeket felhasználhatunk.

Minden olyan műveletet, amelyet egy kritikus szakaszban kell elhelyezni, csak egy szálon érhető el, és bármilyen tranzakciós objektumot használhat, be kell csomagolni a StmUtils.atomic () módszer. A kritikus szakasz egy olyan programhely, amelyet egynél több szál nem futtathat egyszerre, ezért a hozzáférést valamilyen szinkronizációs mechanizmusnak kell védenie.

Ha egy tranzakción belül egy művelet sikeres, a tranzakció végrehajtásra kerül, és az új állapot hozzáférhető lesz más szálak számára. Ha valamilyen hiba történik, a tranzakciót nem hajtják végre, ezért az állam nem változik.

Végül, ha két szál ugyanazt az állapotot akarja módosítani egy tranzakción belül, csak egy sikerrel jár, és végrehajtja annak módosításait. A következő szál a tranzakción belül tudja végrehajtani a műveletét.

4. Fióklogika megvalósítása az STM használatával

Most nézzünk meg egy példát.

Tegyük fel, hogy egy bankszámla logikát akarunk létrehozni a Multiverzum könyvtár. A mi Számla objektum lesz a lastUpadate időbélyeg, amely a TxnLong típus, és a egyensúly mező, amely az adott számla aktuális egyenlegét tárolja, és a TxnInteger típus.

A TxnLong és TxnInteger osztályok a Multiverzum. Egy tranzakción belül kell végrehajtani őket. Ellenkező esetben kivételt dobunk. Használnunk kell a StmUtils a tranzakciós objektumok új példányainak létrehozásához:

public class Account {private TxnLong lastUpdate; privát TxnInteger egyenleg; public account (int egyenleg) {this.lastUpdate = StmUtils.newTxnLong (System.currentTimeMillis ()); this.balance = StmUtils.newTxnInteger (egyenleg); }}

Ezután létrehozzuk a adjustBy () módszer - amely növeli az egyenleget az adott összeggel. Ezt a műveletet tranzakción belül kell végrehajtani.

Ha valamilyen kivétel merül fel benne, akkor a tranzakció változások nélkül véget ér:

public void adjustBy (int összeg) {adjustBy (összeg, System.currentTimeMillis ()); } public void adjustBy (int összeg, hosszú dátum) {StmUtils.atomic (() -> {egyenleg.növekedés (összeg); lastUpdate.set (dátum); if (balance.get () <= 0) {dobjon új IllegalArgumentException ("Nem elég pénz"); } }); }

Ha meg akarjuk szerezni az adott számla aktuális egyenlegét, akkor az egyenleg mezőből kell megszereznünk az értéket, de ezt atomi szemantikával is meg kell hívni:

public Integer getBalance () {return balance.atomicGet (); }

5. A Számla tesztelése

Teszteljük a mi Számla logika. Először a számla egyenlegét szeretnénk csökkenteni a megadott összeggel egyszerűen:

@Test public void givenAccount_whenDecrement_thenShouldReturnProperValue () {Account a = new Account (10); a.adjustBy (-5); assertThat (a.getBalance ()). isEqualTo (5); }

Ezután tegyük fel, hogy kivonulunk a számláról, így az egyenleg negatív lesz. Ennek a műveletnek kivételt kell tennie, és a fiókot érintetlenül kell hagynia, mert a műveletet egy tranzakción belül hajtották végre és nem követték el:

@Test (várható = IllegalArgumentException.class) public void givenAccount_whenDecrementTooMuch_thenShouldThrow () {// adott fiók a = új fiók (10); // amikor a.adjustBy (-11); } 

Teszteljünk most egy párhuzamossági problémát, amely akkor merülhet fel, amikor két szál egyszerre akarja csökkenteni az egyenleget.

Ha az egyik szál akarja csökkenteni 5-tel, a második pedig 6-tal, akkor e két művelet egyike meghiúsulhat, mert az adott számla jelenlegi egyenlege egyenlő 10-vel.

Két szálat fogunk benyújtani a ExecutorService, és használja a CountDownLatch hogy egyszerre indítsam őket:

ExecutorService ex = Executors.newFixedThreadPool (2); A számla = új számla (10); CountDownLatch countDownLatch = new CountDownLatch (1); AtomicBoolean kivételThrown = new AtomicBoolean (hamis); ex.submit (() -> {try {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} try {a.adjustBy (-6);} catch (IllegalArgumentException e) {kivételTrown. meg (igaz);}}); ex.submit (() -> {try {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} try {a.adjustBy (-5);} catch (IllegalArgumentException e) {kivételTrown. meg (igaz);}});

Miután egyszerre bámulta mindkét akciót, egyikük kivételt vet:

countDownLatch.countDown (); ex.awaitTermination (1, TimeUnit.SECONDS); pl. leállítás (); assertTrue (kivételThrown.get ());

6. Átutalás egyik számláról a másikra

Tegyük fel, hogy pénzt akarunk átutalni az egyik számláról a másikra. Meg tudjuk valósítani a transferTo () módszer a Számla osztály a másik elhaladásával Számla amelyre át akarjuk utalni az adott pénzösszeget:

public void transferTo (Számla egyéb, int összeg) {StmUtils.atomic (() -> {hosszú dátum = System.currentTimeMillis (); adjustBy (-összeg, dátum); egyéb.adjustBy (összeg, dátum);}); }

Minden logikát egy tranzakción belül hajtanak végre. Ez garantálja, hogy amikor az adott számlán lévő egyenlegnél nagyobb összeget akarunk átutalni, mindkét számla sértetlen, mert a tranzakció nem vállal kötelezettséget.

Teszteljük a logika átadását:

A számla = új számla (10); B számla = új számla (10); a.transferTo (b, 5); assertThat (a.getBalance ()). isEqualTo (5); assertThat (b.getBalance ()). isEqualTo (15);

Egyszerűen két fiókot hozunk létre, a pénzt egyikről a másikra utaljuk, és minden a várakozásoknak megfelelően működik. Ezután tegyük fel, hogy több pénzt akarunk utalni, mint amennyi a számlán rendelkezésre áll. A transferTo () hívás dobja a IllegalArgumentException, és a változtatásokat nem hajtják végre:

próbáld ki a {a.transferTo (b, 20); } catch (IllegalArgumentException e) {System.out.println ("nem sikerült átutalni a pénzt"); } assertThat (a.getBalance ()). isEqualTo (5); assertThat (b.getBalance ()). isEqualTo (15);

Ne feledje, hogy a mérleg mindkét esetben a és b számlák megegyeznek a transferTo () módszer.

7. Az STM holtpontból biztonságos

Amikor a szokásos Java szinkronizációs mechanizmust használjuk, logikánk hajlamos lehet holtpontokra, anélkül, hogy kilábalnánk belőlük.

A holtpont akkor következhet be, amikor át akarjuk utalni a pénzt a számláról a tart valaminek, tekint valaminek b. A szokásos Java-implementációban egy szálnak zárolnia kell a fiókot a, majd számla b. Tegyük fel, hogy időközben a másik szál át akarja utalni a pénzt a számláról b tart valaminek, tekint valaminek a. A másik szál zárolja a fiókot b számlát vár a hogy feloldják.

Sajnos a fiók zárolása a az első szál és a számla zárja tartja b a második szál tartja. Ilyen helyzet miatt a programunk korlátlan ideig blokkolódik.

Szerencsére a megvalósítás során transferTo () logika az STM segítségével, nem kell aggódnunk a holtpontok miatt, mivel az STM holtpontos. Teszteljük ezt a mi segítségével transferTo () módszer.

Tegyük fel, hogy két szálunk van. Az első szál pénzt akar átutalni a számláról a tart valaminek, tekint valaminek b, a második szál pedig pénzt akar átutalni a számláról b tart valaminek, tekint valaminek a. Két fiókot kell létrehoznunk, és el kell indítanunk két szálat, amelyek végrehajtják a transferTo () módszer egyidejűleg:

ExecutorService ex = Executors.newFixedThreadPool (2); A számla = új számla (10); B számla = új számla (10); CountDownLatch countDownLatch = new CountDownLatch (1); ex.submit (() -> {try {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} a.transferTo (b, 10);}); ex.submit (() -> {try {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} b.transferTo (a, 1);});

A feldolgozás megkezdése után mindkét fiók megkapja a megfelelő egyenleg mezőt:

countDownLatch.countDown (); ex.awaitTermination (1, TimeUnit.SECONDS); pl. leállítás (); assertThat (a.getBalance ()). isEqualTo (1); assertThat (b.getBalance ()). isEqualTo (19);

8. Következtetés

Ebben az oktatóanyagban megnéztük a Multiverzum könyvtár és hogyan használhatjuk fel ezt a zár nélküli és szálbiztos logika létrehozására a szoftver tranzakciós memóriájában található fogalmak felhasználásával.

Kipróbáltuk a megvalósított logika viselkedését, és láttuk, hogy az STM-et használó logika holtpontmentes.

Ezeknek a példáknak és kódrészleteknek a megvalósítása megtalálható a GitHub projektben - ez egy Maven projekt, ezért könnyen importálhatónak és futtathatónak kell lennie.