JavaでのMutexオブジェクトの使用

1. 概要

このチュートリアルでは、* mutexをJavaに実装するさまざまな方法*を紹介します。

2. ミューテックス

マルチスレッドアプリケーションでは、2つ以上のスレッドが共有リソースに同時にアクセスする必要があり、予期しない動作が発生する場合があります。 そのような共有リソースの例は、データ構造、入出力デバイス、ファイル、およびネットワーク接続です。
このシナリオを_レース条件_と呼びます。 そして、共有リソースにアクセスするプログラムの部分は、_クリティカルセクション_として知られています。 *したがって、競合状態を回避するには、クリティカルセクションへのアクセスを同期する必要があります。*
ミューテックス(または相互排除)は、最も単純な_synchronizerのタイプです。* 1つのスレッドのみがコンピュータープログラムのクリティカルセクションを一度に実行できることを保証します*。
クリティカルセクションにアクセスするには、スレッドがミューテックスを取得してから、クリティカルセクションにアクセスし、最後にミューテックスを解放します。 その間、*他のすべてのスレッドはミューテックスが解放されるまでブロックします。*スレッドがクリティカルセクションを出るとすぐに、別のスレッドがクリティカルセクションに入ることができます。

3. ミューテックスを選ぶ理由

まず、_SequenceGeneraror_クラスの例を見てみましょう。このクラスは、毎回_currentValue_を1つずつインクリメントして次のシーケンスを生成します。
public class SequenceGenerator {

    private int currentValue = 0;

    public int getNextSequence() {
        currentValue = currentValue + 1;
        return currentValue;
    }

}
次に、テストケースを作成して、複数のスレッドが同時にアクセスしようとしたときにこのメソッドがどのように動作するかを確認します。
@Test
public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception {
    int count = 1000;
    Set<Integer> uniqueSequences = getUniqueSequences(new SequenceGenerator(), count);
    Assert.assertEquals(count, uniqueSequences.size());
}

private Set<Integer> getUniqueSequences(SequenceGenerator generator, int count) throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(3);
    Set<Integer> uniqueSequences = new LinkedHashSet<>();
    List<Future<Integer>> futures = new ArrayList<>();

    for (int i = 0; i < count; i++) {
        futures.add(executor.submit(generator::getNextSequence));
    }

    for (Future<Integer> future : futures) {
        uniqueSequences.add(future.get());
    }

    executor.awaitTermination(1, TimeUnit.SECONDS);
    executor.shutdown();

    return uniqueSequences;
}
このテストケースを実行すると、次のような理由でほとんどの場合失敗することがわかります。
java.lang.AssertionError: expected:<1000> but was:(989)
  at org.junit.Assert.fail(Assert.java:88)
  at org.junit.Assert.failNotEquals(Assert.java:834)
  at org.junit.Assert.assertEquals(Assert.java:645)
_uniqueSequences_のサイズは、テストケースで_getNextSequence_メソッドを実行した回数に等しいと想定されています。 ただし、競合状態のため、これは当てはまりません。 明らかに、この動作は望ましくありません。
そのため、このような競合状態を回避するには、*一度に1つのスレッドのみが_getNextSequence_メソッドを実行できるようにする必要があります*。 このようなシナリオでは、ミューテックスを使用してスレッドを同期できます。
さまざまな方法がありますが、Javaでミューテックスを実装できます。 したがって、次に、_SequenceGenerator_クラスにミューテックスを実装するさまざまな方法を見ていきます。

4. _synchronized_キーワードの使用

まず、https://www.baeldung.com/java-synchronized [_synchronized_ keyword]について説明します。これは、mutexをJavaで実装する最も簡単な方法です。
Javaのすべてのオブジェクトには、固有のロックが関連付けられています。 ** __ synchronized_メソッドと* * _synchronized_ブロックは、この組み込みロックを使用して、クリティカルセクションへのアクセスを一度に1つのスレッドのみに制限します。
したがって、スレッドが_synchronized_メソッドを呼び出すか、_synchronized_ブロックに入ると、自動的にロックを取得します。 メソッドまたはブロックが完了するか、それらから例外がスローされると、ロックが解除されます。
単に_synchronized_キーワードを追加して、_getNextSequence_をmutexに変更しましょう。
public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {

    @Override
    public synchronized int getNextSequence() {
        return super.getNextSequence();
    }

}
_synchronized_ブロックは_synchronized_メソッドに似ていますが、クリティカルセクションとロックに使用できるオブジェクトをより詳細に制御できます。
それでは、* _ synchronized_ブロックを使用してカスタムミューテックスオブジェクトで同期する方法*を見てみましょう。
public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {

    private Object mutex = new Object();

    @Override
    public int getNextSequence() {
        synchronized (mutex) {
            return super.getNextSequence();
        }
    }

}

5. _ReentrantLock_の使用

_https://www.baeldung.com/java-concurrent-locks [ReentrantLock] _クラスはJava 1.5で導入されました。 _synchronized_キーワードアプローチよりも高い柔軟性と制御を提供します。
_ReentrantLock_を使用して相互排除を実現する方法を見てみましょう。
public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator {

    private ReentrantLock mutex = new ReentrantLock();

    @Override
    public int getNextSequence() {
        try {
            mutex.lock();
            return super.getNextSequence();
        } finally {
            mutex.unlock();
        }
    }
}

6. _Semaphore_の使用

_ReentrantLock_と同様に、_https://www.baeldung.com/java-semaphore [Semaphore] _クラスもJava 1.5で導入されました。
ミューテックスの場合、1つのスレッドのみがクリティカルセクションにアクセスできますが、_Semaphore_では*固定数のスレッドがクリティカルセクションにアクセスできます*。 したがって、* _ Semaphore_で許可されるスレッドの数を1に設定することで、ミューテックスを実装することもできます。
_Semaphore_を使用して、別のスレッドセーフバージョンの_SequenceGenerator_を作成しましょう。
public class SequenceGeneratorUsingSemaphore extends SequenceGenerator {

    private Semaphore mutex = new Semaphore(1);

    @Override
    public int getNextSequence() {
        try {
            mutex.acquire();
            return super.getNextSequence();
        } catch (InterruptedException e) {
            // exception handling code
        } finally {
            mutex.release();
        }
    }
}

7. Guavaの_Monitor_クラスの使用

これまで、Javaが提供する機能を使用してミューテックスを実装するオプションを見てきました。
ただし、GoogleのGuavaライブラリの_Monitor_クラスは、_ReentrantLock_クラスのより良い代替手段です。 https://guava.dev/releases/19.0/api/docs/com/google/common/util/concurrent/Monitor.html [ドキュメント]に従って、_Monitor_を使用したコードはコードより読みやすく、エラーが発生しにくい_ReentrantLock_を使用します。
まず、https://search.maven.org/search?q = g:com.google.guava%20AND%20a:guava [Guava]のMaven依存関係を追加します。
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.0-jre</version>
</dependency>
ここで、_Monitor_クラスを使用して、_SequenceGenerator_の別のサブクラスを作成します。
public class SequenceGeneratorUsingMonitor extends SequenceGenerator {

    private Monitor mutex = new Monitor();

    @Override
    public int getNextSequence() {
        mutex.enter();
        try {
            return super.getNextSequence();
        } finally {
            mutex.leave();
        }
    }

}

8. 結論

このチュートリアルでは、ミューテックスの概念を検討しました。 また、Javaで実装するさまざまな方法を見てきました。
いつものように、このチュートリアルで使用されるコード例の完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-concurrency-2[GitHubで入手可能] ]。