JCToolsを使ったJava並行処理ユーティリティ

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環境でこのクラスを使用することはお勧めしません。

  • NonBlockingHashMapLongNonBlockingHashMapに似ていますが、プリミティブなlongキーを使用します

  • NonBlockingHashSetは、JDKのjava.util.Collections.newSetFromMap()のようなNonBlockingHashMap の単純なラッパーです。

  • NonBlockingIdentityHashMapNonBlockingHashMapと似ていますが、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にあります。