JCToolsを使用したJava同時実行ユーティリティ
1. 概要
このチュートリアルでは、JCTools(Java同時実行ツール)ライブラリを紹介します。
簡単に言えば、これはマルチスレッド環境での作業に適した多くのユーティリティデータ構造を提供します。
2. ノンブロッキングアルゴリズム
Traditionally, multi-threaded code which works on a mutable shared state uses locksは、データの整合性と公開(1つのスレッドによって行われた変更で別のスレッドに表示される)を保証します。
このアプローチにはいくつかの欠点があります。
-
ロックを取得しようとしてスレッドがブロックされ、別のスレッドの操作が終了するまで進行しない場合があります。これにより、並列処理が効果的に防止されます。
-
ロックの競合が重いほど、JVMがスレッドのスケジューリング、競合の管理、待機中のスレッドのキューの処理に費やす時間が長くなり、実際の作業が少なくなります。
-
複数のロックが関係し、間違った順序で取得/解放された場合、デッドロックが発生する可能性
-
priority inversionの危険が発生する可能性があります–優先度の低いスレッドがロックを保持しようとして、優先度の高いスレッドがロックされます
-
ほとんどの場合、粗粒度ロックが使用され、並列処理が大幅に損なわれます。細粒度ロックでは、より慎重な設計が必要で、ロックのオーバーヘッドが増加し、エラーが発生しやすくなります。
別の方法は、non-blocking algorithm, i.e. an algorithm where failure or suspension of any thread cannot cause failure or suspension of another threadを使用することです。
関係するスレッドの少なくとも1つが任意の期間にわたって進行することが保証されている場合、非ブロッキングアルゴリズムはlock-freeです。 処理中にデッドロックが発生することはありません。
さらに、スレッドごとの進行が保証されている場合、これらのアルゴリズムはwait-freeです。
これは、優れたJava Concurrency in PracticeブックのノンブロッキングStackの例です。基本状態を定義します。
public class ConcurrentStack {
AtomicReference> top = new AtomicReference>();
private static class Node {
public E item;
public Node next;
// standard constructor
}
}
また、いくつかのAPIメソッド:
public void push(E item){
Node newHead = new Node(item);
Node oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while(!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node oldHead;
Node newHead;
do {
oldHead = top.get();
if (oldHead == null) {
return null;
}
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
アルゴリズムはきめ細かいコンペアアンドスワップ(CAS)命令を使用し、lock-freeであることがわかります(複数のスレッドが同時にtop.compareAndSet()を呼び出す場合でも、そのうちの1つはCASが特定のスレッドで最終的に成功するという保証はないため、wait-freeではありません。
3. 依存
まず、JCToolsの依存関係をpom.xmlに追加しましょう。
org.jctools
jctools-core
2.1.2
利用可能な最新バージョンはMaven Centralで利用可能であることに注意してください。
4. JCToolsキュー
ライブラリには、マルチスレッド環境で使用するための多数のキューがあります。 1つ以上のスレッドがキューに書き込み、1つ以上のスレッドがスレッドセーフなロックフリー方式でキューから読み取ります。
すべてのQueue実装に共通のインターフェースは、org.jctools.queues.MessagePassingQueueです。
4.1. キューの種類
すべてのキューは、プロデューサー/コンシューマーポリシーで分類できます。
-
single producer, single consumer –のようなクラスは、プレフィックスSpscを使用して名前が付けられます。例: SpscArrayQueue
-
single producer, multiple consumers –はSpmcプレフィックスを使用します。例: SpmcArrayQueue
-
multiple producers, single consumer –はMpscプレフィックスを使用します。例: MpscArrayQueue
-
multiple producers, multiple consumers –はMpmcプレフィックスを使用します。例: MpmcArrayQueue
there are no policy checks internally, i.e. a queue might silently misfunction in case of incorrect usageに注意することが重要です。
E.g. 以下のテストは、2つのスレッドからsingle-producerキューにデータを入力し、コンシューマーが異なるプロデューサーからのデータを表示することが保証されていない場合でも合格します。
SpscArrayQueue queue = new SpscArrayQueue<>(2);
Thread producer1 = new Thread(() -> queue.offer(1));
producer1.start();
producer1.join();
Thread producer2 = new Thread(() -> queue.offer(2));
producer2.start();
producer2.join();
Set fromQueue = new HashSet<>();
Thread consumer = new Thread(() -> queue.drain(fromQueue::add));
consumer.start();
consumer.join();
assertThat(fromQueue).containsOnly(1, 2);
4.2. キューの実装
上記の分類を要約すると、JCToolsキューのリストは次のとおりです。
-
SpscArrayQueue –の単一のプロデューサー、単一のコンシューマーは、内部で配列を使用し、容量を制限します
-
SpscLinkedQueue –の単一のプロデューサー、単一のコンシューマー、内部でリンクリストを使用、バインドされていない容量
-
SpscChunkedArrayQueue –の単一のプロデューサー、単一のコンシューマーは、初期容量から始まり、最大容量まで増加します
-
SpscGrowableArrayQueue –の単一のプロデューサー、単一のコンシューマーは、初期容量から始まり、最大容量まで増加します。 これはSpscChunkedArrayQueueと同じコントラクトですが、唯一の違いは内部チャンク管理です。 実装が簡略化されているため、SpscChunkedArrayQueueを使用することをお勧めします
-
SpscUnboundedArrayQueue –の単一のプロデューサー、単一のコンシューマーは、内部で配列を使用し、容量は制限されていません
-
SpmcArrayQueue –の単一のプロデューサー、複数のコンシューマーは、内部で配列を使用し、容量を制限します
-
MpscArrayQueue –複数のプロデューサー、単一のコンシューマー、内部で配列を使用、容量を制限
-
MpscLinkedQueue –複数のプロデューサー、単一のコンシューマー、リンクリストを内部で使用、バインドされていない容量
-
MpmcArrayQueue –複数のプロデューサー、複数のコンシューマーは、内部で配列を使用し、容量を制限します
4.3. アトミックキュー
前のセクションで説明したすべてのキューはsun.misc.Unsafeを使用します。 ただし、Java 9とJEP-260の出現により、このAPIはデフォルトでアクセスできなくなります。
そのため、sun.misc.Unsafeの代わりにjava.util.concurrent.atomic.AtomicLongFieldUpdater(パブリックAPI、パフォーマンスが低い)を使用する代替キューがあります。
これらは上記のキューから生成され、名前の間にAtomicという単語が挿入されています。例: SpscChunkedAtomicArrayQueueまたはMpmcAtomicArrayQueue。
可能であれば「通常の」キューを使用し、HotSpot Java9 +やJRockitなどのsun.misc.Unsafeが禁止または無効な環境でのみ、AtomicQueuesに頼ることをお勧めします。
4.4. 容量
すべてのJCToolsキューには、最大容量がある場合とバインドされていない場合があります。 When a queue is full and it’s bound by capacity, it stops accepting new elements.
次の例では、次のとおりです。
-
キューを埋める
-
その後、新しい要素の受け入れを停止します
-
そこから排出し、後で要素を追加できるようにします
読みやすくするために、いくつかのコードステートメントが削除されていることに注意してください。 完全な実装はon GitHubで見つけることができます:
SpscChunkedArrayQueue queue = new SpscChunkedArrayQueue<>(8, 16);
CountDownLatch startConsuming = new CountDownLatch(1);
CountDownLatch awakeProducer = new CountDownLatch(1);
Thread producer = new Thread(() -> {
IntStream.range(0, queue.capacity()).forEach(i -> {
assertThat(queue.offer(i)).isTrue();
});
assertThat(queue.offer(queue.capacity())).isFalse();
startConsuming.countDown();
awakeProducer.await();
assertThat(queue.offer(queue.capacity())).isTrue();
});
producer.start();
startConsuming.await();
Set fromQueue = new HashSet<>();
queue.drain(fromQueue::add);
awakeProducer.countDown();
producer.join();
queue.drain(fromQueue::add);
assertThat(fromQueue).containsAll(
IntStream.range(0, 17).boxed().collect(toSet()));
5. その他のJCToolsデータ構造
JCToolsは、キュー以外のデータ構造もいくつか提供します。
それらはすべて以下のとおりです。
-
NonBlockingHashMap –は、より優れたスケーリング特性と一般的に低いミューテーションコストを備えたロックフリーのConcurrentHashMap代替手段です。 sun.misc.Unsafeを介して実装されるため、HotSpot Java9 +またはJRockit環境でこのクラスを使用することはお勧めしません。
-
NonBlockingHashMapLong –はNonBlockingHashMapに似ていますが、プリミティブなlongキーを使用します
-
NonBlockingHashSet –は、JDKのjava.util.Collections.newSetFromMap()のようなNonBlockingHashMap の単純なラッパーです。
-
NonBlockingIdentityHashMap –はNonBlockingHashMapと似ていますが、IDによってキーを比較します。
-
NonBlockingSetInt – aプリミティブlongsの配列として実装されたマルチスレッドビットベクトルセット。 サイレントオートボクシングの場合は効果がありません
6. 性能試験
JDKのArrayBlockingQueueとを比較するためにJMHを使用しましょう。 JCToolsキューのパフォーマンス。 JMHは、Sun / Oracle JVMの達人によるオープンソースのマイクロベンチマークフレームワークであり、コンパイラ/ jvm最適化アルゴリズムの不確定性から私たちを保護します。 詳細については、this articleでお気軽に入手してください。
以下のコードスニペットでは、読みやすくするためにいくつかのステートメントが欠落しています。 GitHub:で完全なソースコードを見つけてください
public class MpmcBenchmark {
@Param({PARAM_UNSAFE, PARAM_AFU, PARAM_JDK})
public volatile String implementation;
public volatile Queue queue;
@Benchmark
@Group(GROUP_NAME)
@GroupThreads(PRODUCER_THREADS_NUMBER)
public void write(Control control) {
// noinspection StatementWithEmptyBody
while (!control.stopMeasurement && !queue.offer(1L)) {
// intentionally left blank
}
}
@Benchmark
@Group(GROUP_NAME)
@GroupThreads(CONSUMER_THREADS_NUMBER)
public void read(Control control) {
// noinspection StatementWithEmptyBody
while (!control.stopMeasurement && queue.poll() == null) {
// intentionally left blank
}
}
}
結果(95パーセンタイルの抜粋、操作ごとのナノ秒):
MpmcBenchmark.MyGroup:MyGroup·p0.95 MpmcArrayQueue sample 1052.000 ns/op
MpmcBenchmark.MyGroup:MyGroup·p0.95 MpmcAtomicArrayQueue sample 1106.000 ns/op
MpmcBenchmark.MyGroup:MyGroup·p0.95 ArrayBlockingQueue sample 2364.000 ns/op
MpmcArrayQueue performs just slightly better than MpmcAtomicArrayQueue and ArrayBlockingQueue is slower by a factor of two.がわかります
7. JCToolsを使用することの欠点
JCToolsの使用には重要な欠点があります–it’s not possible to enforce that the library classes are used correctly.たとえば、大規模で成熟したプロジェクトでMpscArrayQueueの使用を開始するときの状況を考えてみます(単一のコンシューマーが必要であることに注意してください)。
残念ながら、プロジェクトが大きいため、誰かがプログラミングまたは構成エラーを起こし、キューが複数のスレッドから読み取られる可能性があります。 このシステムは以前と同じように動作するようですが、今では消費者がメッセージを見逃す可能性があります。 これは大きな影響を与える可能性がある本当の問題であり、デバッグが非常に困難です。
理想的には、特定のシステムプロパティを使用してシステムを実行し、JCToolsにスレッドアクセスポリシーを保証させることができます。 E.g. local/test/staging environments (but not production) might have it turned on. 悲しいことに、JCToolsはそのようなプロパティを提供していません。
もう1つの考慮事項は、JCToolsがJDKの対応するものよりも大幅に高速であることを確認したとしても、カスタムキュー実装の使用を開始したときと同じ速度でアプリケーションが得られるわけではないということです。 ほとんどのアプリケーションは、スレッド間で多くのオブジェクトを交換せず、ほとんどがI / Oバウンドです。
8. 結論
これで、JCToolsが提供するユーティリティクラスの基本を理解し、負荷の高いJDKの対応するクラスと比較してパフォーマンスが向上することを確認しました。
結論として、it’s worth to use the library only if we exchange a lot of objects between threads and even then it’s necessary to be very careful to preserve thread access policy.
いつものように、上記のサンプルの完全なソースコードはover on GitHubにあります。