並列データ処理とパフォーマンス
Java 7 より前は、データ収集の並列処理は非常に面倒でした。まず、データを含むデータ構造をサブパートに明示的に分割する必要があります。次に、各サブセクションに個別のスレッドを与えます。3 番目に、望ましくない競合状態を避けるために適切なタイミングで同期し、すべてのスレッドが完了するのを待ち、最後に部分的な結果をマージする必要があります。Java 7 では、これらの操作をより安定させ、エラーを発生させにくくするために、ブランチ/マージと呼ばれるフレームワークが導入されています。 この章では、Stream インターフェイスを使用して、それほど手間をかけずにデータ セットに対して並列操作を実行できる方法を学びます。これを使用すると、逐次ストリームを宣言的に並列ストリームに変換できます。さらに、Java がどのように魔法を行うか、より具体的に言えば、Java 7 で導入されたブランチ/マージ フレームワークを使用してストリームがバックグラウンドでどのように機能するかがわかります。また、並列ストリームが内部でどのように動作するかを理解することも重要であることがわかります。この側面を無視すると、誤用により予期しない (おそらく誤った) 結果が生じる可能性があるためです。特に、並列ストリームが並列処理される前にデータのチャンクに分割される方法が、場合によってはこれらの誤った説明できない結果の正確な原因であることを示します。したがって、独自の Spliterator を実装して使用することによって、この分割プロセスを制御する方法を学習します。
並列データ処理
Stream インターフェイスを使用すると、その要素を非常に便利に処理できることについて簡単に説明しました。コレクション ソースでParallelStream メソッドを呼び出すことによって、コレクションを並列ストリームに変換できます。並列ストリームは、コンテンツを複数のデータ ブロックに分割し、異なるスレッドを使用して各データ ブロックを個別に処理するストリームです。このようにして、特定の操作のワークロードをマルチコア プロセッサのすべてのコアに自動的に分散し、すべてのコアをビジー状態に保つことができます。
public void demo1(){
// 生成自然数无限流,对前1000个数字求和
Long reduce = Stream.iterate(1L, i -> i + 1)
.limit(1000)
.reduce(0L, Long::sum);
System.out.println(reduce);
// 用更为传统的Java术语来说,这段代码与下面的迭代等价:
// public static long iterativeSum(long n) {
// long result = 0;
// for (long i = 1L; i <= n; i++) {
// result += i;
// }
// return result;
// }
// 你可以把流转换成并行流,从而让前面的函数归约过程(也就是求和)并行运行——对顺序流调用parallel方法:
Long reduceParallel = Stream.iterate(1L, i -> i + 1)
.limit(1000)
.parallel()
.reduce(0L, Long::sum);
System.out.println(reduceParallel);
/**
* 请注意,在现实中,对顺序流调用parallel方法并不意味着流本身有任何实际的变化。它
* 在内部实际上就是设了一个boolean标志,表示你想让调用parallel之后进行的所有操作都并
* 行执行。类似地,你只需要对并行流调用sequential方法就可以把它变成顺序流。请注意,你
* 可能以为把这两个方法结合起来,就可以更细化地控制在遍历流时哪些操作要并行执行,哪些要
* 顺序执行。例如,你可以这样做:
*/
ArrayList<User> userList = Lists.newArrayList();
userList.add(User.builder().sex("女").name("小红").type(true).uId(10).build());
userList.add(User.builder().sex("女").name("小花").type(false).uId(11).build());
userList.add(User.builder().sex("男").name("小张").type(true).uId(12).build());
userList.add(User.builder().sex("男").name("小网").type(false).uId(13).build());
userList.add(User.builder().sex("男").name("小里").type(true).uId(14).build());
Integer reduceSequential = userList.stream().parallel()
.filter(val -> val.getUId() > 10)
.sequential()
.map(user -> user.getUId())
.limit(3)
.parallel()
.reduce(0, Integer::sum);
System.out.println(reduceSequential);
/**
* 留意装箱。自动装箱和拆箱操作会大大降低性能。Java 8中有原始类型流(IntStream、
* LongStream、DoubleStream)来避免这种操作,但凡有可能都应该用这些流。
*
* 要考虑流背后的数据结构是否易于分解。例如,ArrayList的拆分效率比LinkedList
* 高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历
*/
}
図 7-3 分岐/マージのプロセス
お気づきかもしれませんが、これは有名な分割統治アルゴリズムの単なる並列バージョンです。ここでは、ブランチ/マージ フレームワークを使用する実際的な例を示します。前の例に基づいて、このフレームワークを使用して数値範囲 (ここでは、long[] 配列で表されます) を合計してみます。前に述べたように、最初に RecursiveTask クラスの実装を作成する必要があります。これは、以下のコード リストの ForkJoinSumCalculator です。
public static long forkJoinSum(long n) {
long[] numbers = LongStream.rangeClosed(1, n).toArray();
ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
return new ForkJoinPool().invoke(task);
}
Run ForkJoinSum電卓
スプリッテレータ インターフェイス
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
いつものように、T は Spliterator が通過する要素のタイプです。tryAdvance メソッドは、Spliterator 内の要素を 1 つずつ順番に使用し、反復する他の要素がある場合は true を返すという点で、通常の Iterator と同様に動作します。ただし、trySplit は Spliterator インターフェイス用に特別に設計されており、一部の要素を分割して 2 番目の Spliterator (このメソッドによって返される) に割り当てることができるため、要素を並列処理できます。
Spliterator は、estimateSize メソッドを介して走査する必要がある要素の数を推定することもできます。それほど正確ではない場合でも、値をすばやく計算できると、分割をより均一にするのに役立ちます。必要に応じて制御できるように、このバンドル解除プロセスが内部でどのように実行されるかを理解することが重要です。
最後に独自のSpliterator を実装します...