Transaktionsspeicher der Software in Java mit Multiversum

Software-Transaktionsspeicher in Java mit Multiverse

1. Überblick

In diesem Artikel beschäftigen wir uns mit derMultiverse-Bibliothek, mit deren Hilfe wir das Konzept vonSoftware Transactional Memory in Java implementieren können.

Mit Konstrukten aus dieser Bibliothek können wir einen Synchronisationsmechanismus für den gemeinsam genutzten Status erstellen - eine elegantere und lesbarere Lösung als die Standardimplementierung mit der Java-Kernbibliothek.

2. Maven-Abhängigkeit

Um loszulegen, müssen wir diemultiverse-core-Bibliothek zu unserem pom hinzufügen:


    org.multiverse
    multiverse-core
    0.7.0

3. Multiverse API

Beginnen wir mit einigen Grundlagen.

Software Transactional Memory (STM) ist ein Konzept, das aus der SQL-Datenbankwelt portiert wurde. Dabei wird jede Operation innerhalb von Transaktionen ausgeführt, die die Eigenschaften vonACID (Atomicity, Consistency, Isolation, Durability)erfüllen. Hieronly Atomicity, Consistency and Isolation are satisfied because the mechanism runs in-memory.

The main interface in the Multiverse library is theTxnObject - Jedes Transaktionsobjekt muss es implementieren, und die Bibliothek stellt uns eine Reihe spezifischer Unterklassen zur Verfügung, die wir verwenden können.

Jede Operation, die in einem kritischen Abschnitt platziert werden muss, auf den nur ein Thread zugreifen kann und der ein beliebiges Transaktionsobjekt verwendet, muss innerhalb derStmUtils.atomic()-Methode eingeschlossen werden. Ein kritischer Abschnitt ist eine Stelle eines Programms, die nicht von mehr als einem Thread gleichzeitig ausgeführt werden kann. Der Zugriff auf dieses Programm sollte daher durch einen Synchronisationsmechanismus geschützt werden.

Wenn eine Aktion innerhalb einer Transaktion erfolgreich ist, wird die Transaktion festgeschrieben, und auf den neuen Status können andere Threads zugreifen. Wenn ein Fehler auftritt, wird die Transaktion nicht festgeschrieben, und daher ändert sich der Status nicht.

Wenn zwei Threads denselben Status innerhalb einer Transaktion ändern möchten, ist nur einer erfolgreich und übernimmt die Änderungen. Der nächste Thread kann seine Aktion innerhalb seiner Transaktion ausführen.

4. Implementieren der Kontologik mit STM

Let’s now have a look at an example.

Angenommen, wir möchten eine Bankkontologik mit STM erstellen, die von derMultiverse-Bibliothek bereitgestellt wird. UnserAccount-Objekt hat den ZeitstempellastUpadatevom TypTxnLongund das Feldbalance, in dem der aktuelle Kontostand für ein bestimmtes Konto gespeichert ist und derTxnIntegerentspricht ) s Typ.

DieTxnLong undTxnInteger sind Klassen aus denMultiverse. Sie müssen innerhalb einer Transaktion ausgeführt werden. Andernfalls wird eine Ausnahme ausgelöst. Wir müssenStmUtils verwenden, um neue Instanzen der Transaktionsobjekte zu erstellen:

public class Account {
    private TxnLong lastUpdate;
    private TxnInteger balance;

    public Account(int balance) {
        this.lastUpdate = StmUtils.newTxnLong(System.currentTimeMillis());
        this.balance = StmUtils.newTxnInteger(balance);
    }
}

Als Nächstes erstellen wir die MethodeadjustBy(), mit der der Saldo um den angegebenen Betrag erhöht wird. Diese Aktion muss innerhalb einer Transaktion ausgeführt werden.

Wenn eine Ausnahme ausgelöst wird, wird die Transaktion ohne Änderung beendet:

public void adjustBy(int amount) {
    adjustBy(amount, System.currentTimeMillis());
}

public void adjustBy(int amount, long date) {
    StmUtils.atomic(() -> {
        balance.increment(amount);
        lastUpdate.set(date);

        if (balance.get() <= 0) {
            throw new IllegalArgumentException("Not enough money");
        }
    });
}

Wenn wir den aktuellen Kontostand für das angegebene Konto abrufen möchten, müssen wir den Wert aus dem Feld balance abrufen, er muss jedoch auch mit der Atomsemantik aufgerufen werden:

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

5. Konto testen

Testen wir die Logik vonAccount. Zunächst möchten wir den Kontostand einfach um den angegebenen Betrag verringern:

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

    assertThat(a.getBalance()).isEqualTo(5);
}

Nehmen wir als nächstes an, wir ziehen vom Konto ab und machen den Saldo negativ. Diese Aktion sollte eine Ausnahme auslösen und das Konto intakt lassen, da die Aktion innerhalb einer Transaktion ausgeführt und nicht festgeschrieben wurde:

@Test(expected = IllegalArgumentException.class)
public void givenAccount_whenDecrementTooMuch_thenShouldThrow() {
    // given
    Account a = new Account(10);

    // when
    a.adjustBy(-11);
}

Testen wir nun ein Parallelitätsproblem, das auftreten kann, wenn zwei Threads gleichzeitig einen Kontostand verringern möchten.

Wenn ein Thread ihn um 5 und der zweite um 6 dekrementieren möchte, sollte eine dieser beiden Aktionen fehlschlagen, da der aktuelle Kontostand des angegebenen Kontos gleich 10 ist.

Wir werden zwei Threads an dieExecutorService senden und dieCountDownLatch verwenden, um sie gleichzeitig zu starten:

ExecutorService ex = Executors.newFixedThreadPool(2);
Account a = new Account(10);
CountDownLatch countDownLatch = new CountDownLatch(1);
AtomicBoolean exceptionThrown = new AtomicBoolean(false);

ex.submit(() -> {
    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    try {
        a.adjustBy(-6);
    } catch (IllegalArgumentException e) {
        exceptionThrown.set(true);
    }
});
ex.submit(() -> {
    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    try {
        a.adjustBy(-5);
    } catch (IllegalArgumentException e) {
        exceptionThrown.set(true);
    }
});

Nachdem Sie beide Aktionen gleichzeitig gestartet haben, löst einer von ihnen eine Ausnahme aus:

countDownLatch.countDown();
ex.awaitTermination(1, TimeUnit.SECONDS);
ex.shutdown();

assertTrue(exceptionThrown.get());

6. Übertragen von einem Konto auf ein anderes

Nehmen wir an, wir möchten Geld von einem Konto auf das andere überweisen. Wir können dietransferTo()-Methode für dieAccount-Klasse implementieren, indem wir die anderenAccount übergeben, an die wir den angegebenen Geldbetrag überweisen möchten:

public void transferTo(Account other, int amount) {
    StmUtils.atomic(() -> {
        long date = System.currentTimeMillis();
        adjustBy(-amount, date);
        other.adjustBy(amount, date);
    });
}

Die gesamte Logik wird innerhalb einer Transaktion ausgeführt. Dies garantiert, dass beide Konten intakt sind, wenn wir einen Betrag überweisen möchten, der höher als der Kontostand auf dem angegebenen Konto ist, da die Transaktion nicht festgeschrieben wird.

Testen wir die Übertragungslogik:

Account a = new Account(10);
Account b = new Account(10);

a.transferTo(b, 5);

assertThat(a.getBalance()).isEqualTo(5);
assertThat(b.getBalance()).isEqualTo(15);

Wir erstellen einfach zwei Konten, wir überweisen das Geld von einem zum anderen und alles funktioniert wie erwartet. Nehmen wir als nächstes an, wir möchten mehr Geld überweisen, als auf dem Konto verfügbar ist. Der Aufruf vontransferTo() löstIllegalArgumentException, aus und die Änderungen werden nicht festgeschrieben:

try {
    a.transferTo(b, 20);
} catch (IllegalArgumentException e) {
    System.out.println("failed to transfer money");
}

assertThat(a.getBalance()).isEqualTo(5);
assertThat(b.getBalance()).isEqualTo(15);

Beachten Sie, dass der Saldo für die Kontena undb der gleiche ist wie vor dem Aufruf der MethodetransferTo().

7. STM ist Deadlock-sicher

Wenn wir den Standard-Java-Synchronisationsmechanismus verwenden, kann unsere Logik zu Deadlocks neigen, ohne dass eine Wiederherstellung möglich ist.

Der Deadlock kann auftreten, wenn wir das Geld von Kontoa auf Kontob überweisen möchten. In der Standard-Java-Implementierung muss ein Thread das Kontoa und dann das Kontob sperren. Nehmen wir an, der andere Thread möchte in der Zwischenzeit das Geld von Kontob auf Kontoa überweisen. Der andere Thread sperrt Kontob und wartet darauf, dass ein Kontoa entsperrt wird.

Leider wird die Sperre für ein Kontoa vom ersten Thread und die Sperre für das Kontob vom zweiten Thread gehalten. In diesem Fall wird unser Programm auf unbestimmte Zeit blockiert.

Glücklicherweise müssen wir uns bei der Implementierung dertransferTo()-Logik mit STM keine Gedanken über Deadlocks machen, da das STM Deadlock-sicher ist. Testen wir dies mit unserertransferTo()-Methode.

Nehmen wir an, wir haben zwei Threads. Der erste Thread möchte etwas Geld von Kontoa auf Kontob überweisen, und der zweite Thread möchte etwas Geld von Kontob auf Kontoa überweisen. Wir müssen zwei Konten erstellen und zwei Threads starten, die die MethodetransferTo()gleichzeitig ausführen:

ExecutorService ex = Executors.newFixedThreadPool(2);
Account a = new Account(10);
Account b = new Account(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);

});

Nach dem Beginn der Verarbeitung haben beide Konten das richtige Saldofeld:

countDownLatch.countDown();
ex.awaitTermination(1, TimeUnit.SECONDS);
ex.shutdown();

assertThat(a.getBalance()).isEqualTo(1);
assertThat(b.getBalance()).isEqualTo(19);

8. Fazit

In diesem Tutorial haben wir uns dieMultiverse-Bibliothek angesehen und erklärt, wie wir damit sperrenfreie und threadsichere Logik unter Verwendung von Konzepten im Software-Transaktionsspeicher erstellen können.

Wir haben das Verhalten der implementierten Logik getestet und festgestellt, dass die Logik, die den STM verwendet, nicht blockiert.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inGitHub project - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.