web-dev-qa-db-ja.com

並列ストリーム、コレクター、スレッドセーフ

リスト内の各Wordの出現回数をカウントする以下の簡単な例を参照してください。

Stream<String> words = Stream.of("a", "b", "a", "c");
Map<String, Integer> wordsCount = words.collect(toMap(s -> s, s -> 1,
                                                      (i, j) -> i + j));

最後に、wordsCount{a=2, b=1, c=1}

しかし、私のストリームは非常に大きいので、ジョブを並列化したいので、次のように書きます。

Map<String, Integer> wordsCount = words.parallel()
                                       .collect(toMap(s -> s, s -> 1,
                                                      (i, j) -> i + j));

しかし、wordsCountは単純なHashMapであることに気づいたので、スレッドの安全性を確保するために並行マップを明示的に要求する必要があるのか​​どうか疑問に思います。

Map<String, Integer> wordsCount = words.parallel()
                                       .collect(toConcurrentMap(s -> s, s -> 1,
                                                                (i, j) -> i + j));

非並行コレクターを並列ストリームで安全に使用できますか、または並列ストリームから収集するときに並行バージョンのみを使用する必要がありますか?

39
assylias

非並行コレクターを並行ストリームで安全に使用できますか、または並行ストリームから収集する場合に並行バージョンのみを使用する必要がありますか?

並行ストリームのcollect操作で非並行コレクターを使用しても安全です。

Collectorインターフェイスの specification で、6ダースの箇条書きのセクションで、これは次のとおりです。

非並行コレクターの場合、結果サプライヤ、アキュムレーター、またはコンバイナー関数から返される結果は、連続してスレッド制限される必要があります。これにより、コレクターが追加の同期を実装する必要なく、コレクションを並行して実行できます。リダクション実装は、入力が適切にパーティション化され、パーティションが分離して処理され、蓄積が完了した後にのみ結合が行われることを管理する必要があります。

つまり、Collectorsクラスによって提供されるさまざまな実装は、パラレルストリームで使用できますが、それらの実装の一部は並行コレクターではない場合があります。これは、実装する可能性がある独自の非並行コレクターにも適用されます。コレクターがストリームソースに干渉せず、副作用がなく、順序に依存しないなど、パラレルストリームで安全に使用できます。

また、Java.util.streamパッケージドキュメントの Mutable Reduction セクションを読むことをお勧めします。このセクションの中央には、並列化可能と記載されているが、結果をArrayListに収集する例がありますが、これはスレッドセーフではありません。

これが機能する方法は、非並行コレクターで終わる並列ストリームにより、中間結果コレクションの異なるインスタンスで異なるスレッドが常に動作していることを確認することです。これが、コレクターがスレッドと同数の中間コレクションを作成するためのSupplier関数を持っている理由です。そのため、各スレッドは独自のスレッドに蓄積できます。中間結果をマージする場合、それらはスレッド間で安全に引き渡され、常に1つのスレッドのみが中間結果のペアをマージしています。

42
Stuart Marks

すべてのコレクターは、仕様の規則に従っている場合、並列または順次実行しても安全です。ここでは、並列準備が設計の重要な部分です。

並行コレクターと非並行コレクターの区別は、並列化のアプローチに関係しています。

通常の(非並行)コレクターは、サブ結果をマージして動作します。したがって、ソースは多数のチャンクに分割され、各チャンクは結果コンテナ(リストやマップなど)に収集され、サブ結果はより大きな結果コンテナにマージされます。これは安全で順序を保持しますが、2つのマップをキーでマージするとコストがかかる場合が多いため、一部の種類のコンテナー(特にマップ)にはコストがかかる場合があります。

代わりに、並行コレクターは1つの結果コンテナーを作成します。このコンテナーの挿入操作はスレッドセーフであることが保証され、複数のスレッドから要素を爆発させます。 ConcurrentHashMapのような非常に同時の結果コンテナーを使用すると、このアプローチは通常のHashMapをマージするよりも優れたパフォーマンスを発揮します。

したがって、並行コレクターは、通常のコレクターよりも厳密に最適化されています。そして、彼らは費用なしで来ません。要素は多くのスレッドからブラストされるため、通常、同時コレクターは遭遇順序を維持できません。 (ただし、多くの場合、気にしない-ワードカウントヒストグラムを作成する場合、最初にカウントした "foo"のインスタンスは気にしません。)

19
Brian Goetz

並行ストリームで非並行コレクションおよび非アトミックカウンターを使用しても安全です。

Stream :: collect のドキュメントを見ると、次の段落があります。

reduce(Object, BinaryOperator)と同様に、追加の同期を必要とせずに収集操作を並列化できます。

そして、メソッドの場合 Stream :: reduce

これは、ループ内で実行中の合計を単純に変更する場合に比べて、集約を実行するためのより遠回りの方法に見えるかもしれませんが、削減操作は、追加の同期を必要とせず、データ競合のリスクを大幅に削減しながら、より優雅に並列化します。

これは少し驚くかもしれません。ただし、並列ストリームfork-joinモデルに基づいていることに注意してください 。つまり、同時実行は次のように機能します。

  • ほぼ同じサイズの2つの部分にシーケンスを分割します
  • 各部分を個別に処理する
  • 両方の部分の結果を収集し、それらを1つの結果に結合します

2番目のステップでは、3つのステップがサブシーケンスに再帰的に適用されます。

例はそれを明確にする必要があります。の

IntStream.range(0, 4)
    .parallel()
    .collect(Trace::new, Trace::accumulate, Trace::combine);

クラスTraceの唯一の目的は、コンストラクターとメソッド呼び出しを記録することです。このステートメントを実行すると、次の行が出力されます。

thread:  9  /  operation: new
thread: 10  /  operation: new
thread: 10  /  operation: accumulate
thread:  1  /  operation: new
thread:  1  /  operation: accumulate
thread:  1  /  operation: combine
thread: 11  /  operation: new
thread: 11  /  operation: accumulate
thread:  9  /  operation: accumulate
thread:  9  /  operation: combine
thread:  9  /  operation: combine

4つのTraceオブジェクトが作成され、accumulateが作成されていることがわかります各オブジェクトで1回呼び出され、combineを3回使用して、4つのオブジェクトを1つに結合しました。各オブジェクトには、一度に1つのスレッドのみがアクセスできます。これにより、コードはスレッドセーフになり、メソッドCollectors :: toMapにも同じことが当てはまります。

10
nosid