Java SynchronousQueueのガイド
1. 概要
この記事では、java.util.concurrentパッケージのSynchronousQueueについて説明します。
簡単に言えば、この実装により、スレッドセーフな方法でスレッド間で情報を交換できます。
2. APIの概要
SynchronousQueueにはtwo supported operations: take() and put(), and both of them are blockingしかありません。
たとえば、キューに要素を追加する場合は、put()メソッドを呼び出す必要があります。 そのメソッドは、他のスレッドがtake()メソッドを呼び出して、要素を取得する準備ができていることを通知するまでブロックします。
SynchronousQueueにはキューのインターフェイスがありますが、1つのスレッドが要素を渡し、別のスレッドがその要素を受け取る2つのスレッド間の単一要素の交換ポイントと考える必要があります。
3. 共有変数を使用したハンドオフの実装
SynchronousQueueが非常に役立つ理由を理解するために、2つのスレッド間で共有変数を使用してロジックを実装し、次に、SynchronousQueueを使用してそのロジックを書き直し、コードを非常に単純で読みやすくします。
プロデューサーとコンシューマーの2つのスレッドがあり、プロデューサーが共有変数の値を設定しているときに、その事実をコンシューマースレッドに通知するとします。 次に、コンシューマスレッドは共有変数から値を取得します。
CountDownLatchを使用して、これら2つのスレッドを調整し、コンシューマーがまだ設定されていない共有変数の値にアクセスする状況を防ぎます。
処理の調整に使用されるsharedState変数とCountDownLatchを定義します。
ExecutorService executor = Executors.newFixedThreadPool(2);
AtomicInteger sharedState = new AtomicInteger();
CountDownLatch countDownLatch = new CountDownLatch(1);
プロデューサーはランダムな整数をsharedState変数に保存し、sharedState:から値をフェッチできることをコンシューマーに通知するcountDownLatch,に対してcountDown()メソッドを実行します。
Runnable producer = () -> {
Integer producedElement = ThreadLocalRandom
.current()
.nextInt();
sharedState.set(producedElement);
countDownLatch.countDown();
};
コンシューマーは、await()メソッドを使用してcountDownLatchを待機します。 プロデューサーが変数が設定されたことを通知すると、コンシューマーはsharedState:から変数をフェッチします
Runnable consumer = () -> {
try {
countDownLatch.await();
Integer consumedElement = sharedState.get();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
};
最後になりましたが、プログラムを始めましょう。
executor.execute(producer);
executor.execute(consumer);
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
executor.shutdown();
assertEquals(countDownLatch.getCount(), 0);
次のような出力が生成されます。
Saving an element: -1507375353 to the exchange point
consumed an element: -1507375353 from the exchange point
これは、2つのスレッド間で要素を交換するような単純な機能を実装するための多くのコードであることがわかります。 次のセクションでは、改善を試みます。
4. SynchronousQueueを使用したハンドオフの実装
前のセクションと同じ機能を実装しましょう。ただし、SynchronousQueue.を使用します。スレッド間で状態を交換したり、アクションを調整したりして何も使用する必要がないため、二重の効果があります。 SynchronousQueue.以外
まず、キューを定義します。
ExecutorService executor = Executors.newFixedThreadPool(2);
SynchronousQueue queue = new SynchronousQueue<>();
プロデューサーは、他のスレッドがキューから要素を取得するまでブロックするput()メソッドを呼び出します。
Runnable producer = () -> {
Integer producedElement = ThreadLocalRandom
.current()
.nextInt();
try {
queue.put(producedElement);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
};
コンシューマーは、take()メソッドを使用してその要素を取得するだけです。
Runnable consumer = () -> {
try {
Integer consumedElement = queue.take();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
};
次に、プログラムを開始します。
executor.execute(producer);
executor.execute(consumer);
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
executor.shutdown();
assertEquals(queue.size(), 0);
次のような出力が生成されます。
Saving an element: 339626897 to the exchange point
consumed an element: 339626897 from the exchange point
スレッド間の交換ポイントとしてSynchronousQueueが使用されていることがわかります。これは、共有状態をCountDownLatch.と一緒に使用した前の例よりもはるかに優れて理解しやすいものです。
5. 結論
このクイックチュートリアルでは、SynchronousQueueコンストラクトを確認しました。 共有状態を使用して2つのスレッド間でデータを交換するプログラムを作成し、そのプログラムを書き直してSynchronousQueue構造を活用しました。 これは、生産者スレッドと消費者スレッドを調整する交換ポイントとして機能します。
これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。