Java8 実践的な戦闘 [第 7 章] 並列データ処理とパフォーマンス、分岐/マージ フレームワーク、Parallel()、Sequential()、Spliterator インターフェイス、Spliterator を使用したストリームの分割

並列データ処理とパフォーマンス

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 です。

これで、最初のn 個の 自然数を並列して合計するメソッドを 簡単に書くことができます。必要な数値の配列を渡すだけです
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電卓

ForkJoinSumCalculator タスクが ForkJoinPool に渡されると、タスクはプール内のスレッドによって実行されます。
実行されると、このスレッドはタスクの compute メソッドを呼び出します。このメソッドは、タスクが連続して実行できるほど小さいかどうかを確認し、そうでない場合は
それが十分に小さい場合、合計される配列は 2 つの半分に分割され、2 つの新しい ForkJoinSumCalculator に分割され、さらに次の方法で除算されます。
ForkJoinPool は実行をスケジュールします。したがって、このプロセスは再帰的に繰り返すことができ、元のタスクを小さなタスクに分割して、
これ以上の分割が不都合または不可能な条件 (この場合、合計される項目の数が 10,000 以下)。このとき、配列が計算されます。
各タスクの結果が計算され、分岐プロセスによって作成されたタスクの (暗黙的な) バイナリ ツリーがそのルートまでたどられます。次に会う
各サブタスクの部分的な結果を結合して、タスク全体の結果を取得します。このプロセスを図 7-4 に示します。

 

スプリッテレータ インターフェイス

Spliterator は Java 8 で追加されたもう 1 つの新しいインターフェイスで、名前は「分割可能なイテレータ」の略です。
イテレータ)。Iterator と同様に、Spliterator もデータ ソース内の要素を走査するために使用されますが、並列実行用に設計されています。
そしてデザインされました。実際には、独自の Spliterator を開発する必要はないかもしれませんが、その実装方法を理解することで、
並列ストリームがどのように機能するかをより深く理解します。
Java 8 は、
デフォルトの Spliterator 実装。コレクションは、スプリッテレータ メソッドを提供する Spliterator インターフェイスを実装します。
次のコード リストに示すように、このインターフェイスはいくつかのメソッドを定義します。
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 メソッドを介して走査する必要がある要素の数を推定することもできます。それほど正確ではない場合でも、値をすばやく計算できると、分割をより均一にするのに役立ちます。必要に応じて制御できるように、このバンドル解除プロセスが内部でどのように実行されるかを理解することが重要です。

分割プロセス:
図 7-6 に示すように、ストリームを複数の部分に分割するアルゴリズムは再帰的なプロセスです。最初のステップは、まず
Spliterator は、trySplit を呼び出して 2 番目の Spliterator を生成します。
2 番目のステップでは、2 つの Spliterator に対して trysplit を呼び出すため、合計 4 つの Spliterator が存在します。このフレームワークは、ステップ 3 に示すように、処理中のデータ構造がこれ以上分割できないことを示す null を返すまで、Spliterator で trySplit を呼び出し続けます。最後に、再帰的分割プロセスは 4 番目のステップで終了しますが、この時点では、trySplit を呼び出すと、すべての Spliterator が null を返します。
この分割プロセスは Spliterator 自体の特性にも影響され、その特性は特性メソッドを通じて宣言されます。
明的。
スプリッテレーターの特徴:
Spliterator インターフェイスによって宣言された最後の抽象メソッドはcharacteristics で、これは int を返します。
テーブル Spliterator 自体の機能セットのエンコーディング。Spliterator を使用しているお客様は、これらの機能を使用して、より詳細な制御を得ることができ、
その使用を最適化します。

最後に独自のSpliterator を実装します...

おすすめ

転載: blog.csdn.net/amosjob/article/details/126612708