Software Transactional Memory in Java met behulp van Multiverse

1. Overzicht

In dit artikel zullen we kijken naar de Multiversum bibliotheek - wat ons helpt om het concept van Software Transactioneel Geheugen in Java.

Door constructies uit deze bibliotheek te gebruiken, kunnen we een synchronisatiemechanisme creëren op gedeelde status - wat een elegantere en leesbaardere oplossing is dan de standaardimplementatie met de Java-kernbibliotheek.

2. Maven Afhankelijkheid

Om aan de slag te gaan, moeten we het multiversum-kern bibliotheek in onze pom:

 org.multiverse multiversum-core 0.7.0 

3. Multiverse API

Laten we beginnen met enkele basisprincipes.

Software Transactional Memory (STM) is een concept dat is overgenomen uit de SQL-databasewereld - waar elke bewerking wordt uitgevoerd binnen transacties die voldoen aan ZUUR (Atomiciteit, Consistentie, Isolatie, Duurzaamheid) eigendommen. Hier, alleen aan atomiciteit, consistentie en isolatie wordt voldaan omdat het mechanisme in het geheugen draait.

De belangrijkste interface in de Multiverse-bibliotheek is de TxnObject - elk transactieobject moet het implementeren en de bibliotheek biedt ons een aantal specifieke subklassen die we kunnen gebruiken.

Elke bewerking die binnen een kritieke sectie moet worden geplaatst, toegankelijk is via slechts één thread en met behulp van een transactieobject, moet worden verpakt in de StmUtils.atomic () methode. Een kritieke sectie is een plaats van een programma die niet door meer dan één thread tegelijk kan worden uitgevoerd, dus de toegang ertoe moet worden bewaakt door een of ander synchronisatiemechanisme.

Als een actie binnen een transactie slaagt, wordt de transactie vastgelegd en is de nieuwe status toegankelijk voor andere threads. Als er een fout optreedt, wordt de transactie niet vastgelegd en verandert de staat daarom niet.

Ten slotte, als twee threads dezelfde status binnen een transactie willen wijzigen, zal er maar één slagen en de wijzigingen vastleggen. De volgende thread kan zijn actie binnen zijn transactie uitvoeren.

4. Accountlogica implementeren met STM

Laten we nu een voorbeeld bekijken.

Laten we zeggen dat we een bankrekeninglogica willen maken met behulp van STM van de Multiversum bibliotheek. Onze Account object zal de lastUpadate tijdstempel met een TxnLong type en het balans veld dat het huidige saldo voor een bepaalde rekening opslaat en van de TxnGeheel getal type.

De TxnLong en TxnGeheel getal zijn klassen uit de Multiversum. Ze moeten worden uitgevoerd binnen een transactie. Anders wordt er een uitzondering gegenereerd. We moeten de StmUtils om nieuwe instanties van de transactionele objecten te maken:

openbare klasse Account {privé TxnLong lastUpdate; privé TxnInteger-saldo; openbaar account (int saldo) {this.lastUpdate = StmUtils.newTxnLong (System.currentTimeMillis ()); this.balance = StmUtils.newTxnInteger (saldo); }}

Vervolgens maken we het adjustBy () methode - die het saldo met het opgegeven bedrag verhoogt. Die actie moet worden uitgevoerd binnen een transactie.

Als er een uitzondering in wordt gegooid, wordt de transactie beëindigd zonder enige wijziging door te voeren:

openbare ongeldige adjustBy (int bedrag) {adjustBy (bedrag, System.currentTimeMillis ()); } public void adjustBy (int amount, long date) {StmUtils.atomic (() -> {balance.increment (amount); lastUpdate.set (date); if (balance.get () <= 0) {throw new IllegalArgumentException ("Niet genoeg geld"); } }); }

Als we het huidige saldo voor het gegeven account willen krijgen, moeten we de waarde uit het saldo-veld halen, maar het moet ook worden aangeroepen met atomaire semantiek:

openbaar geheel getal getBalance () {return balance.atomicGet (); }

5. Testen van het account

Laten we onze Account logica. Ten eerste willen we het saldo van de rekening eenvoudig met het opgegeven bedrag verlagen:

@Test openbare ongeldig gegevenAccount_whenDecrement_thenShouldReturnProperValue () {Account a = nieuw account (10); a.adjustBy (-5); assertThat (a.getBalance ()). isEqualTo (5); }

Laten we vervolgens zeggen dat we ons opnemen van de rekening en het saldo negatief maken. Die actie zou een uitzondering moeten genereren en het account intact laten omdat de actie is uitgevoerd binnen een transactie en niet is vastgelegd:

@Test (verwacht = IllegalArgumentException.class) public void givenAccount_whenDecrementTooMuch_thenShouldThrow () {// gegeven account a = nieuw account (10); // wanneer a.adjustBy (-11); } 

Laten we nu een gelijktijdigheidsprobleem testen dat kan optreden wanneer twee threads tegelijkertijd een balans willen verlagen.

Als een thread het met 5 wil verlagen en de tweede met 6, zou een van die twee acties moeten mislukken omdat het huidige saldo van het gegeven account gelijk is aan 10.

We gaan twee threads indienen bij de ExecutorServiceen gebruik de CountDownLatch om ze tegelijkertijd te starten:

ExecutorService ex = Executors.newFixedThreadPool (2); Account a = nieuw account (10); CountDownLatch countDownLatch = nieuwe CountDownLatch (1); AtomicBoolean exceptionThrown = nieuwe AtomicBoolean (false); ex.submit (() -> {probeer {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} probeer {a.adjustBy (-6);} catch (IllegalArgumentException e) {exceptionThrown. set (true);}}); ex.submit (() -> {probeer {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} probeer {a.adjustBy (-5);} catch (IllegalArgumentException e) {exceptionThrown. set (true);}});

Na beide acties tegelijkertijd te hebben bekeken, genereert een van hen een uitzondering:

countDownLatch.countDown (); ex.awaitTermination (1, TimeUnit.SECONDS); ex. afsluiten (); assertTrue (exceptionThrown.get ());

6. Overboeken van de ene rekening naar de andere

Laten we zeggen dat we geld willen overboeken van de ene rekening naar de andere. We kunnen het Overzetten naar() methode op de Account klasse door de andere te passeren Account waarnaar we het gegeven geldbedrag willen overmaken:

public void transferTo (Account andere, int bedrag) {StmUtils.atomic (() -> {lange datum = System.currentTimeMillis (); adjustBy (-bedrag, datum); other.adjustBy (bedrag, datum);}); }

Alle logica wordt uitgevoerd binnen een transactie. Dit garandeert dat wanneer we een bedrag willen overboeken dat hoger is dan het saldo op de betreffende rekening, beide rekeningen intact blijven omdat de transactie niet zal worden vastgelegd.

Laten we de overdrachtslogica testen:

Account a = nieuw account (10); Account b = nieuw account (10); a.transferTo (b, 5); assertThat (a.getBalance ()). isEqualTo (5); assertThat (b.getBalance ()). isEqualTo (15);

We maken gewoon twee accounts aan, we maken het geld over van de ene naar de andere en alles werkt zoals verwacht. Laten we vervolgens zeggen dat we meer geld willen overboeken dan er op de rekening staat. De Overzetten naar() oproep zal de IllegalArgumentException, en de wijzigingen zullen niet worden vastgelegd:

probeer {a.transferTo (b, 20); } catch (IllegalArgumentException e) {System.out.println ("kan geen geld overmaken"); } assertThat (a.getBalance ()). isEqualTo (5); assertThat (b.getBalance ()). isEqualTo (15);

Merk op dat het saldo voor beide een en b accounts is hetzelfde als vóór de aanroep naar de Overzetten naar() methode.

7. STM is veilig in een deadlock

Wanneer we het standaard Java-synchronisatiemechanisme gebruiken, kan onze logica vatbaar zijn voor impasses, zonder dat we deze kunnen herstellen.

De impasse kan optreden wanneer we het geld van de rekening willen overboeken een verklaren b. Bij standaard Java-implementatie moet één thread het account vergrendelen een, dan account b. Laten we zeggen dat de andere thread ondertussen het geld van de rekening wil overboeken b verklaren een. De andere thread vergrendelt account b wachten op een account een worden ontgrendeld.

Helaas is het slot voor een rekening een wordt vastgehouden door de eerste draad, en het slot voor rekening b wordt vastgehouden door de tweede draad. Een dergelijke situatie zorgt ervoor dat ons programma voor onbepaalde tijd wordt geblokkeerd.

Gelukkig, bij het implementeren Overzetten naar() logica met behulp van STM, hoeven we ons geen zorgen te maken over deadlocks, aangezien de STM Deadlock Safe is. Laten we dat testen met onze Overzetten naar() methode.

Laten we zeggen dat we twee threads hebben. Eerste thread wil wat geld overboeken van rekening een verklaren b, en de tweede thread wil wat geld van de rekening overboeken b verklaren een. We moeten twee accounts aanmaken en twee threads starten die het Overzetten naar() methode tegelijkertijd:

ExecutorService ex = Executors.newFixedThreadPool (2); Account a = nieuw account (10); Account b = nieuw account (10); CountDownLatch countDownLatch = nieuwe CountDownLatch (1); ex.submit (() -> {probeer {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} a.transferTo (b, 10);}); ex.submit (() -> {probeer {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} b.transferTo (a, 1);});

Nadat de verwerking is gestart, hebben beide accounts het juiste saldo-veld:

countDownLatch.countDown (); ex.awaitTermination (1, TimeUnit.SECONDS); ex. afsluiten (); assertThat (a.getBalance ()). isEqualTo (1); assertThat (b.getBalance ()). isEqualTo (19);

8. Conclusie

In deze tutorial hebben we de Multiversum bibliotheek en hoe we die kunnen gebruiken om lock-free en thread-safe logica te creëren met behulp van concepten in het Software Transactional Memory.

We hebben het gedrag van de geïmplementeerde logica getest en gezien dat de logica die de STM gebruikt, geen impasse heeft.

De implementatie van al deze voorbeelden en codefragmenten is te vinden in het GitHub-project - dit is een Maven-project, dus het moet gemakkelijk te importeren en uit te voeren zijn zoals het is.