マルチバースを使用したJavaのソフトウェアトランザクションメモリ

1概要

この記事では、 Multiverse ライブラリーを見ていきます。これは、 Software Transactional Memory __の概念をJavaで実装するのに役立ちます。

このライブラリからのコンストラクトを使用して、共有状態で同期メカニズムを作成できます。これは、Javaコアライブラリを使用した標準的な実装よりも洗練された読みやすいソリューションです。

2 Mavenの依存関係

始めるには、https://search.maven.org/classic/#search%7C1%7Cg%3A%22org.multiverse%22%20AND%20a%3A%22multiverse-core%22を追加する必要があります。[ multiverse-core ]ライブラリをpomに追加します。

<dependency>
    <groupId>org.multiverse</groupId>
    <artifactId>multiverse-core</artifactId>
    <version>0.7.0</version>
</dependency>

3マルチバースAPI

いくつかの基本から始めましょう。

ソフトウェアトランザクションメモリ(STM)はSQLデータベースの世界から移植された概念です - 各操作は ACID(Atomicity、Consistency、Isolation、Durability) プロパティを満たすトランザクション内で実行されます。ここでは、メカニズムがメモリ内で実行されるため、Atomicity、Consistency、およびIsolationのみが満たされます。

  • Multiverseライブラリの主なインターフェースは TxnObject ** です - 各トランザクションオブジェクトはそれを実装する必要があり、ライブラリは私たちが使うことができるいくつかの特定のサブクラスを提供してくれます。

1つのスレッドだけがアクセスでき、任意のトランザクションオブジェクトを使用して、クリティカルセクション内に配置する必要がある各操作は、 StmUtils.atomic() メソッド内でラップする必要があります。クリティカルセクションは、複数のスレッドによって同時に実行することができないプログラムの場所です。そのため、それへのアクセスは何らかの同期メカニズムによって保護されるべきです。

トランザクション内のアクションが成功すると、トランザクションはコミットされ、新しい状態は他のスレッドからアクセス可能になります。何らかのエラーが発生した場合、トランザクションはコミットされず、したがって状態は変わりません。

最後に、2つのスレッドがトランザクション内で同じ状態を変更したい場合は、1つだけが成功してその変更をコミットします。次のスレッドはそのトランザクション内でそのアクションを実行できます。

4 STM を使用したアカウントロジックの実装

  • 例を見てみましょう** 。

Multiverse ライブラリによって提供されるSTMを使用して銀行口座ロジックを作成したいとしましょう。 Account オブジェクトには、 TxnLong タイプの lastUpadate タイムスタンプと、特定のアカウントの現在の残高を格納する TxnInteger タイプの balance フィールドがあります。

TxnLong TxnInteger は、 Multiverse のクラスです。それらはトランザクション内で実行されなければなりません。そうでなければ、例外がスローされます。トランザクションオブジェクトの新しいインスタンスを作成するには、 StmUtils を使用する必要があります。

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

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

次に、 adjustBy() メソッドを作成します。これは、残高を指定した量だけ増やします。そのアクションはトランザクション内で実行する必要があります。

その中で例外が投げられると、トランザクションは何も変更せずに終了します。

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");
        }
    });
}

与えられた口座の現在の残高を取得したい場合は、残高フィールドから値を取得する必要がありますが、アトミックセマンティクスを使用して呼び出す必要もあります。

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

5アカウントをテストする

アカウントのロジックをテストしましょう。まず、口座から指定された金額だけ残高を引き下げます。

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

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

次に、残高をマイナスにして、口座から引き落としたとしましょう。アクションはトランザクション内で実行され、コミットされなかったため、そのアクションは例外をスローし、アカウントをそのままにします。

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

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

2つのスレッドが同時に残高を減らす場合に発生する可能性がある並行性の問題をテストしましょう。

1つのスレッドが5を減らし、2番目のスレッドが6を減らしたい場合、指定されたアカウントの現在の残高は10であるため、これら2つのアクションのうちの1つは失敗します。

2つのスレッドを ExecutorService に送信し、 CountDownLatch を使用してそれらを同時に開始します。

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);
    }
});

両方のアクションを同時に見つめた後、どちらかが例外をスローします。

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

assertTrue(exceptionThrown.get());

6. あるアカウントから別のアカウントへの転送

ある口座から別の口座に送金したいとしましょう。

指定された金額を転送したい相手の Account を渡すことで、 transferTo()メソッドを Account__クラスに実装できます。

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

すべてのロジックはトランザクション内で実行されます。これにより、特定の口座の残高よりも多い金額を振り込む場合、トランザクションがコミットされないため、両方の口座が損なわれることはありません。

転送ロジックをテストしましょう。

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

a.transferTo(b, 5);

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

単純に2つのアカウントを作成し、一方から他方へお金を振り込みます。すべてが期待通りに機能します。次に、アカウントで利用可能な金額以上の送金をしたいとしましょう。 transferTo() 呼び出しは、 IllegalArgumentException、 をスローし、変更はコミットされません。

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);

a アカウントと b アカウントの両方の残高は、 transferTo() メソッドを呼び出す前と同じです。

7. STMはデッドロックセーフです

標準のJava同期メカニズムを使用している場合、ロジックからデッドロックが発生する可能性があり、それらから回復することはできません。

デッドロックは、口座 a から口座 b に送金する場合に発生する可能性があります。標準のJava実装では、1つのスレッドがアカウント a をロックしてからアカウント b をロックする必要があります。その間、もう一方のスレッドがアカウント b からアカウント a への送金を希望しているとしましょう。もう一方のスレッドは、アカウント a がロック解除されるのを待って、アカウント b をロックします。

残念ながら、アカウント a のロックは最初のスレッドによって保持され、アカウント b のロックは2番目のスレッドによって保持されています。そのような状況は私達のプログラムを無期限にブロックさせるでしょう。

幸い、STMを使用して transferTo() ロジックを実装する場合、STMはデッドロックセーフなので、デッドロックについて心配する必要はありません。 transferTo() メソッドを使用してそれをテストしましょう。

2つのスレッドがあるとしましょう。最初のスレッドは、アカウント a からアカウント b にいくらかのお金を振り込みたい、そして2番目のスレッドはアカウント b からアカウント a にいくらかのお金を振り込みたいです。 2つのアカウントを作成し、 transferTo() メソッドを同時に実行する2つのスレッドを起動する必要があります。

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);

});

処理を開始すると、両方の勘定に適切な残高項目が表示されます。

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

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

8結論

このチュートリアルでは、 Multiverse ライブラリと、それを使用してソフトウェアトランザクションメモリの概念を利用してロックフリーでスレッドセーフなロジックを作成する方法について説明しました。

実装したロジックの動作をテストしたところ、STMを使用するロジックはデッドロックフリーであることがわかりました。

これらすべての例とコードスニペットの実装はhttps://github.com/eugenp/tutorials/tree/master/libraries[GitHubプロジェクト]にあります。そのまま実行します。