Java8の並列および非同期プログラミング

並列コードは、複数のスレッドで実行されるコードであり、かつては多くの経験豊富な開発者の悪夢でしたが、Java8はこのパフォーマンス向上のトリックをより管理しやすくするために多くの変更をもたらしました。

並列ストリーム

Java8より前には、並列(または並行)コードとシーケンシャルコードの間に大きな違いがありました。 また、非順次コードをデバッグすることも非常に困難でした。 ブレークポイントを設定して通常のようにフローを通過するだけで、並列アスペクトが削除されますが、それがバグの原因である場合は問題です。

幸いなことに、Java8はbean以来のJava開発者にとって最大のstreamsを提供しました。 それらが何であるかわからない場合、Stream APIは機能的な問題で一連の要素を処理することを可能にします。 (ここでstreamsと.NETのLINQの比較を確認してください。)ストリームの利点の1つは、コードの構造が同じままであることです: それがシーケンシャルであろうと同時であろうと、それは読みやすいままです。コードを並列に実行するには、.stream()の代わりに.parallelStream()を使用します(ストリームの作成者でない場合はstream.parallel()を使用します)。

しかし、それが簡単だからといって、並列コードが常に最良の選択であることを意味するものではありません。 コードに並行性を使用することが理にかなっているかどうかを常に検討する必要があります。 その決定の最も重要な要素は速度です:同時実行を使用するのは、コードが順次対応するものよりも高速になる場合にのみ使用してください。

速度の質問

並列コードは、シーケンシャルコードが使用する単一のスレッドの代わりに複数のスレッドを使用することで、その速度の利点を得ます。 スレッド数を増やすとコードが高速になるとは限らないため、作成するスレッドの数を決定するのは難しい質問になる可能性があります。

選択するスレッドの数を教えてくれるルールがいくつかあります。 これは主に、実行する操作の種類と使用可能なコアの数に依存します。

計算集約的な操作では、コア数以下のスレッドを使用する必要がありますが、ファイルのコピーなどのIO集約的な操作ではCPUには使用できないため、より多くのスレッドを使用することができます。 コードは、何をすべきかを指示しない限り、どのケースが適用されるかを知りません。 それ以外の場合は、コアの数に等しいスレッドの数にデフォルト設定されます。

コードをシーケンシャルではなく並列に実行すると便利な主なケースが2つあります。 Java8は、これらの大きなコレクションを処理する新しい方法、すなわちstreamsをもたらしました。 ストリームには、怠惰による効率が組み込まれています。 これは並列処理と同じではありませんが、高速化されている限りリソースを気にしません。 だから、大きなコレクションのために、あなたはおそらく古典的な並列処理を必要としません。

非同期になる

JavaScriptからの教訓

Java開発者がJavaScriptを見て何かを学んだと言うことはまれですが、非同期プログラミングに関しては、JavaScriptが実際に最初にそれ 基本的に非同期言語として、JavaScriptはひどく実装されたときにどれほど苦痛を伴うかについて多くの経験を持っています。 これはコールバックから始まり、後にpromiseに置き換えられました。 Promiseの重要な利点は、データ用とエラー用の2つの「チャネル」があることです。 JavaScriptの約束は次のようになります:

func.then(f1).catch(e1).then(f2).catch(e2);

したがって、元の関数が成功した結果を持つと、f1が呼び出されますが、エラーがスローされた場合はe1が呼び出されます。 これにより、成功したトラック(f2)に戻されるか、別のエラー(e2)が発生する可能性があります。 データトラックからエラートラックに移動して戻ることができます。

JavaScript promiseのJavaバージョンはCompletableFutureと呼ばれています。

CompletableFuture

CompletableFutureFutureCompletionStageの両方のインターフェイスを実装します。 FutureはすでにJava8より前に存在していましたが、それ自体はあまり開発者に優しいものではありませんでした。 非同期計算の結果は、.get()メソッドを使用してのみ取得でき、残りの部分をブロックし(ほとんどの場合、非同期部分をかなり無意味にします)、可能な各シナ CompletionStageインタフェースを追加することは、Javaでの非同期プログラミングを実行可能にした画期的なことでした。

CompletionStageはpromise、つまり計算が最終的に行われるという約束です。 それはあなたがその完了時に実行されるコールバックを添付できるようにするメソッドの束を含んでいます。 これで、ブロックせずに結果を処理できます。

コードの非同期部分を開始できる2つの主なメソッドがあります:メソッドの結果で何かをしたい場合はsupplyAsync、そうでない場合はrunAsync.

CompletableFuture.runAsync(() → System.out.println("Run async in completable future " + Thread.currentThread()));CompletableFuture.supplyAsync(() → 5);

コールバック

今、あなたはあなたのsupplyAsyncの結果を処理するために、これらのコールバックを追加することができます。

CompletableFuture.supplyAsync(() → 5).thenApply(i → i * 3).thenAccept(i → System.out.println("The result is " + i).thenRun(() → System.out.println("Finished."));

.thenApply これはストリームの.map関数に似ています:変換を実行します。 上記の例では、結果(5)を取得し、それに3を乗算します。 その後、その結果(15)をパイプのさらに下に渡します。

.thenAcceptは、結果を変換せずにメソッドを実行します。 また、結果も返されません。 ここでは、コンソールに”結果は15です”と表示されます。 これは、ストリームの.foreachメソッドと比較できます。

.thenRunは非同期操作の結果を使用せず、何も返さず、前のステップが完了するまでRunnableを呼び出すのを待つだけです。

非同期

上記のすべてのコールバックメソッドも非同期バージョンで提供されます:thenRunAsyncthenApplyAsyncなど。 これらのバージョンは独自のスレッドで実行でき、どのForkJoinPoolを使用するかを伝えることができるため、余分な制御を提供します。

非同期バージョンを使用しない場合、コールバックはすべて同じスレッドで実行されます。

何かがうまくいかないとき

何かがうまくいかないとき、exceptionallyメソッドが例外を処理するために使用されます。 データトラックに戻るための値を返すメソッドを指定するか、(新しい)例外をスローすることができます。

….exceptionally(ex → new Foo()).thenAccept(this::bar);

結合と構成

thenComposeメソッドを使用して、複数のCompletableFuturesを連鎖させることができます。 それがなければ、結果はネストされたCompletableFuturesになります。 これにより、ストリームのthenComposethenApplyflatMapmapのようになります。

CompletableFuture.supplyAsync(() -> "Hello").thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "World"));

二つのCompletableFuturesの結果を結合したい場合は、便利にthenCombineと呼ばれるメソッドが必要です。

future.thenCombine(future2, Integer::sum).thenAccept(value → System.out.println(value));

上記の例でわかるように、thenCombineのコールバックの結果は、すべてのお気に入りのCompletionStageメソッドを使用して、通常のCompletableFutureのように処理できます。

結論

並列プログラミングは、より高速なコードの探求において克服できない障害である必要はなくなりました。 Java8はプロセスをできる限り簡単にするので、その恩恵を受ける可能性のあるコードをすべてのスレッドで引っ張って蹴って叫ぶことができます。 私が意味する:それは簡単ですので、試してみて、自分のためにその利点を見てください。



+