1. 概要

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

2. Mutex

マルチスレッドアプリケーションでは、2つ以上のスレッドが同時に共有リソースにアクセスする必要があり、予期しない動作が発生する可能性があります。 このような共有リソースの例としては、データ構造、入出力デバイス、ファイル、およびネットワーク接続があります。

このシナリオを競合状態と呼びます。 そして、共有リソースにアクセスするプログラムの部分は、クリティカルセクションとして知られています。 したがって、競合状態を回避するには、クリティカルセクションへのアクセスを同期する必要があります。

ミューテックス(または相互排除)は、シンクロナイザーの最も単純なタイプです–それは一度に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. 同期キーワードの使用

最初に、同期キーワードについて説明します。これは、Javaでミューテックスを実装する最も簡単な方法です。

Javaのすべてのオブジェクトには、固有のロックが関連付けられています。 同期メソッドおよび同期ブロックは、この組み込みロックを使用して、クリティカルセクションへのアクセスを一度に1つのスレッドのみに制限します。

したがって、スレッドが synchronized メソッドを呼び出すか、 synchronized ブロックに入ると、スレッドは自動的にロックを取得します。 メソッドまたはブロックが完了するか、それらから例外がスローされると、ロックが解放されます。

同期されたキーワードを追加するだけで、getNextSequenceをミューテックスを持つように変更しましょう。

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {
    
    @Override
    public synchronized int getNextSequence() {
        return super.getNextSequence();
    }

}

synchronized ブロックは、 synchronized メソッドに似ていますが、クリティカルセクションとロックに使用できるオブジェクトをより細かく制御できます。

それでは、同期ブロックを使用してカスタムミューテックスオブジェクトで同期する方法を見てみましょう。

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {
    
    private Object mutex = new Object();

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

}

5. ReentrantLockを使用する

ReentrantLockクラスはJava1.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. セマフォの使用

ReentrantLock と同様に、セマフォクラスもJava1.5で導入されました。

ミューテックスの場合、クリティカルセクションにアクセスできるスレッドは1つだけですが、セマフォでは、固定数のスレッドがクリティカルセクションにアクセスできます。 したがって、セマフォで許可されるスレッドの数を1 に設定することで、ミューテックスを実装することもできます。

次に、セマフォを使用して、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クラスよりも優れた代替手段です。 ドキュメントによると、 Monitor を使用したコードは、 ReentrantLock を使用したコードよりも読みやすく、エラーが発生しにくくなっています。

まず、GuavaのMaven依存関係を追加します。

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-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でそれを実装するさまざまな方法を見てきました。

いつものように、このチュートリアルで使用されているコード例の完全なソースコードは、GitHubからで入手できます。