1. 概要

前回の記事で、AtomicStampedReferenceがABA問題を防ぐことができることを学びました。

このチュートリアルでは、それを最適に使用する方法を詳しく見ていきます。

2. なぜAtomicStampedReferenceが必要なのですか?

まず、 AtomicStampedReferenceは、オブジェクト参照変数と、アトミックに読み書きできるスタンプの両方を提供しますスタンプはタイムスタンプやバージョン番号のように考えることができます。

簡単に言うと、スタンプを追加すると、別のスレッドが共有参照を元の参照Aから新しい参照Bに変更し、元の参照Aに戻したことを検出できます。

それが実際にどのように動作するかを見てみましょう。

3. 銀行口座の例

残高と最終更新日という2つのデータがある銀行口座について考えてみます。 最終変更日は、残高が変更されるたびに更新されます。 この最終更新日を確認することで、アカウントが更新されたことを知ることができます。

3.1. 値とそのスタンプを読む

まず、私たちの参照が口座残高を保持していると想像してみましょう。

AtomicStampedReference<Integer> account = new AtomicStampedReference<>(100, 0);

残高100とスタンプ0を提供していることに注意してください。

残高にアクセスするには、 accountメンバー変数でAtomicStampedReference.getReference()メソッドを使用できます。

同様に、 AtomicStampedReference.getStamp()を介してスタンプを取得できます。

3.2. 値とそのスタンプの変更

それでは、AtomicStampedReferenceの値をアトミックに設定する方法を確認しましょう。

アカウントの残高を変更する場合は、残高とスタンプの両方を変更する必要があります。

if (!account.compareAndSet(balance, balance + 100, stamp, stamp + 1)) {
    // retry
}

compareAndSet メソッドは、成功または失敗を示すブール値を返します。 失敗とは、最後に読んだ後に残高またはスタンプのいずれかが変更されたことを意味します。

ご覧のとおり、 ゲッターを使用して参照とスタンプを取得するのは簡単です。

ただし、前述のように、CASを使用して値を更新する場合は、最新バージョンのが必要です。 これらの2つの情報をアトミックに取得するには、それらを同時に取得する必要があります。

幸い、 AtomicStampedReference は、これを実現するための配列ベースのAPIを提供します。 Accountクラスにwithdrawal()メソッドを実装して、その使用法を示しましょう。

public boolean withdrawal(int funds) {
    int[] stamps = new int[1];
    int current = this.account.get(stamps);
    int newStamp = this.stamp.incrementAndGet();
    return this.account.compareAndSet(current, current - funds, stamps[0], newStamp);
}

同様に、deposit()メソッドを追加できます。

public boolean deposit(int funds) {
    int[] stamps = new int[1];
    int current = this.account.get(stamps);
    int newStamp = this.stamp.incrementAndGet();
    return this.account.compareAndSet(current, current + funds, stamps[0], newStamp);
}

私たちが今書いたものの良いところは、引き出したり預けたりする前に、最後に読んだときからの状態に戻っても、他のスレッドがバランスを変更していないことを知ることができることです。

たとえば、次のスレッドインターリーブについて考えてみます。

残高は100ドルに設定されています。 スレッド1はdeposit(100)を次のポイントまで実行します。

int[] stamps = new int[1];
int current = this.account.get(stamps);
int newStamp = this.stamp.incrementAndGet(); 
// Thread 1 is paused here

デポジットがまだ完了していないことを意味します。

次に、スレッド2は預金(100)引き出し(100)を実行し、残高を$ 200に戻し、次に$100に戻します。

最後に、スレッド1が実行されます。

return this.account.compareAndSet(current, current + 100, stamps[0], newStamp);

スレッド1は、残高自体がスレッド1が読み取ったときと同じであっても、他のスレッドが最後の読み取り以降にアカウントの残高を変更したことを正常に検出します。

3.3. テスト

これは非常に特定のスレッドインターリーブに依存するため、テストするのは難しいです。しかし、少なくとも、入金と出金が機能することを確認するための簡単な単体テストを書いてみましょう。

public class ThreadStampedAccountUnitTest {

    @Test
    public void givenMultiThread_whenStampedAccount_thenSetBalance() throws InterruptedException {
        StampedAccount account = new StampedAccount();

        Thread t = new Thread(() -> {
            while (!account.deposit(100)) {
                Thread.yield();
            }
        });
        t.start();

        Thread t2 = new Thread(() -> {
            while (!account.withdrawal(100)) {
                Thread.yield();
            }
        });
        t2.start();

        t.join(10_000);
        t2.join(10_000);

        assertFalse(t.isAlive());
        assertFalse(t2.isAlive());

        assertEquals(0, account.getBalance());
        assertTrue(account.getStamp() > 0);
    }
}

3.4. 次のスタンプの選択

意味的には、スタンプはタイムスタンプまたはバージョン番号のようなものであるため、通常は常に増加します。 乱数ジェネレーターを使用することも可能です。

これは、スタンプを以前のスタンプに変更できる場合、AtomicStampedReferenceの目的が損なわれる可能性があるためです。AtomicStampedReference自体はこの制約を適用しません、したがって、この慣行に従うのは私たち次第です。

4. 結論

結論として、 AtomicStampedReference は、アトミックに読み取りおよび更新できる参照とスタンプの両方を提供する強力な同時実行ユーティリティです。 これはABA検出用に設計されており、ABA問題が懸念されるAtomicReferenceなどの他の同時実行クラスよりも優先される必要があります。

いつものように、GitHubで利用可能なコードを見つけることができます。