java.util.concurrent.BlockingQueueのガイド

java.util.concurrent.BlockingQueueのガイド

1. 概要

この記事では、生産者/消費者の同時問題を解決するために最も有用な構成要素java.util.concurrentの1つを見ていきます。 BlockingQueueインターフェースのAPIと、そのインターフェースのメソッドが並行プログラムの作成をどのように容易にするかを見ていきます。

この記事の後半では、複数のプロデューサースレッドと複数のコンシューマスレッドを持つ単純なプログラムの例を示します。

2. BlockingQueueタイプ

2つのタイプのBlockingQueueを区別できます。

  • 無制限のキュー-ほぼ無限に成長する可能性があります

  • 制限キュー-最大容量が定義されている

2.1. 無制限のキュー

制限のないキューの作成は簡単です:

BlockingQueue blockingQueue = new LinkedBlockingDeque<>();

blockingQueueの容量はInteger.MAX_VALUE.に設定されます。要素を無制限のキューに追加するすべての操作はブロックされないため、非常に大きなサイズになる可能性があります。

無制限のBlockingQueueを使用してプロデューサー-コンシューマープログラムを設計する際の最も重要なことは、プロデューサーがキューにメッセージを追加するのと同じくらい早くコンシューマーがメッセージを消費できることです。 そうしないと、メモリがいっぱいになり、OutOfMemory例外が発生する可能性があります。

2.2. 制限付きキュー

キューの2番目のタイプは、制限キューです。 コンストラクターに引数として容量を渡すことで、このようなキューを作成できます。

BlockingQueue blockingQueue = new LinkedBlockingDeque<>(10);

ここに、容量が10に等しいblockingQueueがあります。 これは、コンシューマーが要素を追加するために使用されたメソッド(offer()add()、またはput())に応じて、すでにいっぱいになっているキューに要素を追加しようとすると、ブロックすることを意味しますオブジェクトを挿入するためのスペースが利用可能になるまで。 そうしないと、操作は失敗します。

すでにキューがいっぱいになっている要素を挿入する場合、その操作は消費者が追いついてキューの一部のスペースを使用できるようになるまで待機する必要があるため、制限付きキューを使用すると、並行プログラムを設計するのに適した方法です。 これにより、私たちは何の努力もせずに調整できます。

3. BlockingQueue API

BlockingQueueインターフェースには、キューへの要素の追加を担当するメソッドと、それらの要素を取得するメソッドの2種類のメソッドがあります。 これらの2つのグループの各メソッドは、キューが満杯/空の場合の動作が異なります。

3.1. 要素を追加する

  • add() –は、挿入が成功した場合はtrueを返し、それ以外の場合はIllegalStateExceptionをスローします。

  • put() –は指定された要素をキューに挿入し、必要に応じて空きスロットを待ちます

  • offer() –は、挿入が成功した場合はtrueを返し、そうでない場合はfalseを返します。

  • offer(E e, long timeout, TimeUnit unit) –は要素をキューに挿入しようとし、指定されたタイムアウト内に使用可能なスロットを待機します

3.2. 要素の取得

  • take() –キューのhead要素を待機して削除します。 キューが空の場合、ブロックされ、要素が利用可能になるのを待ちます

  • poll(long timeout, TimeUnit unit) –はキューの先頭を取得して削除し、必要に応じて要素が使用可能になるまで指定された待機時間まで待機します。 タイムアウト後にnullを返します__

これらのメソッドは、プロデューサー-コンシューマープログラムを構築する際のBlockingQueueインターフェイスからの最も重要なビルディングブロックです。

4. マルチスレッドの生産者/消費者の例

プロデューサーとコンシューマーの2つの部分で構成されるプログラムを作成しましょう。

プロデューサーは0から100までの乱数を生成し、その数をBlockingQueueに入れます。 4つのプロデューサースレッドがあり、put()メソッドを使用して、キューに使用可能なスペースができるまでブロックします。

覚えておくべき重要なことは、消費者スレッドが要素がキューに無期限に現れるのを待つのを止める必要があるということです。

処理するメッセージがもうないことを生産者から消費者に知らせる良い方法は、ポイズンピルと呼ばれる特別なメッセージを送信することです。 私たちは消費者と同数の毒薬を送る必要があります。 次に、コンシューマがキューからその特別なポイズンピルメッセージを取得すると、実行が正常に終了します。

プロデューサークラスを見てみましょう。

public class NumbersProducer implements Runnable {
    private BlockingQueue numbersQueue;
    private final int poisonPill;
    private final int poisonPillPerProducer;

    public NumbersProducer(BlockingQueue numbersQueue, int poisonPill, int poisonPillPerProducer) {
        this.numbersQueue = numbersQueue;
        this.poisonPill = poisonPill;
        this.poisonPillPerProducer = poisonPillPerProducer;
    }
    public void run() {
        try {
            generateNumbers();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private void generateNumbers() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            numbersQueue.put(ThreadLocalRandom.current().nextInt(100));
        }
        for (int j = 0; j < poisonPillPerProducer; j++) {
            numbersQueue.put(poisonPill);
        }
     }
}

プロデューサーコンストラクターは、プロデューサーとコンシューマーの間の処理を調整するために使用されるBlockingQueueを引数として取ります。 メソッドgenerateNumbers()が100個の要素をキューに入れることがわかります。 また、実行が終了するときにキューに入れられるメッセージの種類を知るために、ポイズンピルメッセージも必要です。 そのメッセージは、poisonPillPerProducer回キューに入れる必要があります。

各コンシューマーは、take()メソッドを使用してBlockingQueueから要素を取得するため、キューに要素が存在するまでブロックされます。 キューからIntegerを取得した後、メッセージがポイズンピルであるかどうかを確認し、そうである場合はスレッドの実行が終了します。 それ以外の場合は、現在のスレッドの名前とともに結果が標準出力に出力されます。

これにより、消費者の内面の働きに対する洞察が得られます。

public class NumbersConsumer implements Runnable {
    private BlockingQueue queue;
    private final int poisonPill;

    public NumbersConsumer(BlockingQueue queue, int poisonPill) {
        this.queue = queue;
        this.poisonPill = poisonPill;
    }
    public void run() {
        try {
            while (true) {
                Integer number = queue.take();
                if (number.equals(poisonPill)) {
                    return;
                }
                System.out.println(Thread.currentThread().getName() + " result: " + number);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

注目すべき重要なことは、キューの使用法です。 プロデューサーコンストラクターと同じように、キューは引数として渡されます。 明示的な同期なしでBlockingQueueをスレッド間で共有できるため、これを行うことができます。

プロデューサーとコンシューマーができたので、プログラムを開始できます。 キューの容量を定義する必要があり、それを100要素に設定します。

4つのプロデューサースレッドが必要であり、コンシューマースレッドの数は利用可能なプロセッサーの数に等しくなります。

int BOUND = 10;
int N_PRODUCERS = 4;
int N_CONSUMERS = Runtime.getRuntime().availableProcessors();
int poisonPill = Integer.MAX_VALUE;
int poisonPillPerProducer = N_CONSUMERS / N_PRODUCERS;
int mod = N_CONSUMERS % N_PRODUCERS;

BlockingQueue queue = new LinkedBlockingQueue<>(BOUND);

for (int i = 1; i < N_PRODUCERS; i++) {
    new Thread(new NumbersProducer(queue, poisonPill, poisonPillPerProducer)).start();
}

for (int j = 0; j < N_CONSUMERS; j++) {
    new Thread(new NumbersConsumer(queue, poisonPill)).start();
}

new Thread(new NumbersProducer(queue, poisonPill, poisonPillPerProducer + mod)).start();

BlockingQueueは、容量のある構成を使用して作成されます。 4つのプロデューサーとNのコンシューマーを作成しています。 ポイズンピルメッセージをInteger.MAX_VALUEに指定します。これは、このような値が通常の作業条件下でプロデューサーによって送信されることはないためです。 ここで注意すべき最も重要なことは、BlockingQueueがそれらの間の作業を調整するために使用されることです。

プログラムを実行すると、4つのプロデューサースレッドがランダムなIntegersBlockingQueueに配置し、コンシューマーがそれらの要素をキューから取得します。 各スレッドは、結果とともにスレッドの名前を標準出力に出力します。

5. 結論

この記事では、BlockingQueueの実際の使用法を示し、要素を追加および取得するために使用されるメソッドについて説明します。 また、BlockingQueueを使用してマルチスレッドの生産者/消費者プログラムを構築し、生産者と消費者の間の作業を調整する方法も示しました。

これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。