CompletableFutureの詳しい使い方

JAVAがサポートするマルチスレッドオープンメソッド

Oracle が発行した公式 Java ドキュメントによると、スレッドを作成するには 2 つの方法しかありません。 Thread を継承するか、Runnable インターフェイスを実装するかです。しかし、これらのメソッドにはどちらにも欠陥があり、戻り値がないため、スレッドの実行結果を知ることができません。単純なシナリオでは満たされますが、値を返す必要がある場合はどうすればよいでしょうか? Java 1.5 以降の Callable インターフェイスと Future インターフェイスでは、この問題が解決されています。Callable をスレッド プールに送信することで、戻り値を含む Future オブジェクトを取得できます。それ以降、プログラム ロジックは同期順序ではなくなりました。

以下はJava8戦闘本の原文です。

Future インターフェースは Java5 で導入され、その元の設計は、将来のある時点で生成される結果をモデル化することです。これは非同期操作をモデル化し、実行結果への参照を返し、操作が完了すると呼び出し元に参照を返します。将来、時間のかかる可能性のある操作の完了をトリガーします。
以下の図に示すように、初期のシリアル操作から並列操作に変更しましたが、非同期でありながら、プログラムの実行時間を節約するために他のことも行うことができます。

画像出典:Javaの将来の仕組みを詳しく解説 - ほぼわかる

機知に富んだ友人たちは間違いなく、「この記事は JAVA8 の新機能 CompletableFuture に関するものではないのですか?」と尋ねるでしょう。未来についてどのように話しますか?急ぐ必要はありません。以下を参照してください

Future インターフェースの制限

結果を含む Future を取得したら、get メソッドを使用してスレッドが完了するのを待ち、戻り値を取得できます。Future のget()メソッドは、太字で示した メイン スレッドをブロックすることに注意してください。Future文書の原文は以下の通り

{@code Future} は、非同期計算の結果を表します。計算が完了したかどうかを確認し、完了を待ち、計算結果を取得するためのメソッドが提供されています。

グーグル翻訳:

{@code Future} は、非同期* 計算の結果を表します。計算が完了したかどうかを確認し、完了するまで待機し、計算結果を取得するためのメソッドが提供されています。

Future は時間のかかるタスクを実行します

このことから、Future がスレッドの実行結果を取得する前に、メインスレッドの get() がブロックして結果を待つ必要があることがわかります。スレッドの実行ステータスをチェックするために isDone() メソッドを使用してポーリングしたとしても、これはCPU リソースの無駄。

 

画像出典:Java8実戦

Future スレッドが非常に時間のかかる操作を実行すると、メイン スレッドもブロックされます。単純な作業を行う場合は、Future の別のオーバーロードされたメソッド get(long, TimeUnit) を使用してタイムアウト期間を設定し、メイン スレッドが際限なくブロックされるのを避けることができます。しかし、もっと良い解決策はあるのでしょうか?

より強力な非同期機能が必要です

それだけではなく、ビジネス シナリオに遭遇したときに、Future インターフェイスや FutureTask クラスを使用するだけでは、必要な次のビジネスを十分に完了することはできません。

  • 2 つの非同期計算を 1 つに結合します。2 つの非同期計算は互いに独立しており、2 番目の計算は最初の計算の結果に依存します。
  • Future コレクション内のすべてのタスクが完了するまで待ちます。
  • Future コレクション内の最も速く終了するタスクが完了するのを待って (おそらく、同じ値を別の方法で計算しようとしたため)、その結果を返します。
  • Future タスクの実行をプログラムで完了します (つまり、非同期操作の結果を手動で設定します)。
  • Future の完了時刻に応答します (つまり、Future の完了時刻が完了すると通知され、単に結果をブロックするだけでなく、Future の計算結果を使用して次の操作を実行できます)。手術を待っています)

文章

魔法の完成可能な未来

CompletableFutureとは

Java 8 では、約 50 のメソッドを持つ新しいクラスが追加されました。 CompletableFuture は、Future の利点を組み合わせ、Future の非常に強力な拡張機能を提供します。これは、非同期プログラミングの複雑さを簡素化し、関数型プログラミングを提供するのに役立ちます。コールバックを通じて計算結果を処理し、CompletableFuture を変換および結合するためのメソッドを提供します。

CompletableFuture は、Java での非同期プログラミング用に設計されています。非同期プログラミングとは、メイン スレッドの外側に、メイン スレッドから分離された独立したスレッドを作成し、そこでノンブロッキング タスクを実行し、メイン スレッドに進行状況、成功、または失敗を通知することを意味します。

このようにして、メインスレッドはタスクの完了をブロックしたり待機したりする必要がなく、メインスレッドを使用して他のタスクを並行して実行できます。この並列方法を使用すると、プログラムのパフォーマンスが大幅に向上します。

Java8 ソース コードのドキュメント コメント:

 翻訳

Future を明示的に完了する必要がある場合は、CompletionStage インターフェイスを使用して、完了時にトリガーされる関数と操作をサポートします。
2 つ以上のスレッドが CompletableFuture を同時に完了、例外で完了、またはキャンセルしようとした場合、成功できるのは 1 つだけです。
 
CompletableFuture は、CompletionStage インターフェイスの次の戦略を実装します。
 
1. 現在の CompletableFuture インターフェイスまたは他の完了メソッドのコールバック関数のスレッドを完了するために、非同期完了操作が提供されます。
 
2. Executor に明示的に入らないすべての非同期メソッドは ForkJoinPool.commonPool() を使用します監視、デバッグ、追跡を簡素化するために、
    生成されるすべての非同期タスクはマーカー インターフェイス AsynchronousCompletionTask のインスタンスです。
 
3. すべての CompletionStage メソッドは他のパブリック メソッドとは独立して実装されるため、メソッドの動作がサブクラス内の
    他のメソッドによってオーバーライドされることはありません。
 
CompletableFuture は、Future インターフェイスの次の戦略を実装します。
 
1. CompletableFuture は完了を直接制御できないため、キャンセル操作は異常完了の別の形式とみなされます。
    isCompletedExceptionally メソッドを使用すると、CompletableFuture が異常な方法で完了したかどうかを判断できます。
 
2. CompletionException を例に挙げると、メソッド get() および get(long, TimeUnit) は ExecutionException をスローします。
    CompletionException に相当します。
    ほとんどのコンテキストでの使用を簡素化するために、このクラスは、これらの場合に CompletionException を直接スローする代わりに、メソッド join() および getNow も定義します。

CompletableFuture API

例を直接見つけて始めたい友人は、後ろにスキップしてください

CompletableFuture をインスタンス化する

インスタンス化方法

  1. public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);

  2. public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);

  3. public static CompletableFuture<Void> runAsync(Runnable runnable);

  4. public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);

形式は2つあり、1つはsupplyで始まる方式、もう1つはrunで始まる方式です。

  • 提供開始: このメソッドは非同期スレッドの実行後に結果を返すことができます
  • 実行の開始: これは結果を返さず、スレッドタスクを実行するだけです

または、単純な引数なしのコンストラクターを渡すこともできます

CompletableFuture<String> completableFuture = new CompletableFuture<String>();

ヒント: インスタンス化メソッドでは Executor パラメーターを指定できることに気づきましたが、テストを指定しない場合、開いた並列スレッドはデフォルトのシステムとパブリック スレッド プール ForkJoinPool を使用し、これらのスレッドはすべてデーモン スレッドになります。プログラミング時にはデーモン スレッドを慎重に使用する必要があります。通常のユーザー スレッドをデーモン スレッドとして設定すると、プログラムのメイン スレッドが終了し、JVM に他のユーザー スレッドがなくなったときに、CompletableFuture のデーモン スレッドが直接終了します。原因となっているタスクを完了できない問題、およびデーモン スレッドのブロック問題を含む残りの部分については、この記事では繰り返しません。

Java8の実戦:

このうち、supplyAsync は戻り値のあるタスクに使用され、runAsync は戻り値のないタスクに使用されます。Executor パラメータはスレッド プールを手動で指定できます。それ以外の場合は、ForkJoinPool.commonPool() のシステム レベルの共通スレッド プールがデフォルト設定されます。注: これらのスレッドはすべてデーモン スレッドです。メイン スレッドが終了してもデーモン スレッドは終了しません。メイン スレッドが終了したときのみ、 JVM が閉じられると、ライフサイクルが終了します。

結果を得る

結果を同期的に取得する

public T    get()
public T    get(long timeout, TimeUnit unit)
public T    getNow(T valueIfAbsent)
public T    join()

簡単な例

CompletableFuture<Integer> future = new CompletableFuture<>();
Integer integer = future.get();

get() メソッドもタスクが完了するまでブロックされます。上記のコードでは、この方法で作成されたフューチャーが完了したことがないため、メインスレッドは常にブロックされます。興味のある友達はブレークポイントを作成して確認できます。ステータスは常に完了していません

最初の 2 つの方法は比較的理解しやすいので、上記の「将来」のセクションを読んだ人は、その意味を理解しているはずです。 getNow() は異なり、パラメータ valueIfAbsent は、計算結果が存在しない場合、またはタスクが現時点で完了していない場合に、特定の値を与えることを意味します。

join() とget() の違いは、join() は 計算結果を返すか、未チェックの例外 (CompletionException) をスローするのに対し、get() は特定の例外を返すことです。

計算完了後、後続操作1——完了

public CompletableFuture<T>     whenComplete(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T>     whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T>     whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
public CompletableFuture<T>     exceptionally(Function<Throwable,? extends T> fn)

メソッド 1 と 2 の違いは、非同期処理を使用するかどうかであり、メソッド 2 と 3 の違いは、カスタム スレッド プールを使用するかどうかです。最初の 3 つのメソッドは、返される結果とスローされる例外を提供します。受け取るラムダ式 これらの 2 つのパラメーターは、単独で処理されます。メソッド 4、スロー可能な例外を受け取り、戻り値を返す必要があります。型はダイヤモンド式の型と同じです。 詳細については、以下の例外的に()セクションを参照してください。

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            return 10086;
        });
        future.whenComplete((result, error) -> {
            System.out.println("拨打"+result);
            error.printStackTrace();
        });

計算完了後、後続操作2——ハンドル

public <U> CompletableFuture<U>     handle(BiFunction<? super T,Throwable,? extends U> fn)
public <U> CompletableFuture<U>     handleAsync(BiFunction<? super T,Throwable,? extends U> fn)
public <U> CompletableFuture<U>     handleAsync(BiFunction<? super T,Throwable,? extends U> fn, Executor executor)

目の鋭い友人は、ハンドル メソッド セットが上記の完全なメソッド セットと何ら変わらないことに気づいたかもしれません。戻り値とスロー可能な例外という 2 つのパラメータもあります。違いは戻り値にあります。はい、戻り値も戻りますCompletableFuture 型ですが、内部のパラメーターの型とハンドル メソッドはカスタマイズできます。

// 开启一个异步方法
        CompletableFuture<List> future = CompletableFuture.supplyAsync(() -> {
            List<String> list = new ArrayList<>();
            list.add("语文");
            list.add("数学");
            // 获取得到今天的所有课程
            return list;
        });
        // 使用handle()方法接收list数据和error异常
        CompletableFuture<Integer> future2 = future.handle((list,error)-> {
            // 如果报错,就打印出异常
            error.printStackTrace();
            // 如果不报错,返回一个包含Integer的全新的CompletableFuture
            return list.size();
             // 注意这里的两个CompletableFuture包含的返回类型不同
        });

計算完了後の後操作3——適用

public <U> CompletableFuture<U>     thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U>     thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U>     thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

これら 3 つのメソッドが計算完了後のフォローアップ操作 2 と呼ばれるのは、apply メソッドが計算終了後のフォローアップ操作である handle メソッドと同じであるためです。唯一の違いは次のとおりです。 handle メソッドは例外を発生させ、ユーザーは内部で処理され、apply メソッドの戻り値は 1 つだけです。例外が発生した場合は、直接スローされ、処理のために上位層に渡されます。チェーン呼び出しごとに例外を処理したくない場合は、apply を使用します。

:以下の例外的に() の 例を参照してください。

計算完了後の後続操作 4 —— 承諾

public CompletableFuture<Void>  thenAccept(Consumer<? super T> action)
public CompletableFuture<Void>  thenAcceptAsync(Consumer<? super T> action)
public CompletableFuture<Void>  thenAcceptAsync(Consumer<? super T> action, Executor executor)

accept() メソッドは最終結果のみを使用します。この時点で返される CompletableFuture は空であることに注意してください。消費のみで返品はありません。ストリーミング プログラミングの最終操作に少し似ています

:以下の例外的に() の 例を参照してください。

途中で生成された例外をキャッチします - 例外的に

public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

Excellently() は、すべての中間プロセス例外をキャッチするのに役立ちます。このメソッドは、パラメータとして例外を提供します。この例外を処理して、サービスの低下に似たデフォルト値を返すことができます。デフォルト値の型 は前の操作と同じで、戻り値も同じです。 ヒント : タスクをスレッド プールに送信するときに発生する例外は外部例外であり、タスクはまだ実行されていないため、キャッチできません。作成者は、スレッド プール拒否ポリシーがトリガーされたときにもこの問題を発見しました。例外的に() はRejectedExecutionException() をキャッチできません

// 实例化一个CompletableFuture,返回值是Integer
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            // 返回null
            return null;
        });
 
        CompletableFuture<String> exceptionally = future.thenApply(result -> {
            // 制造一个空指针异常NPE
            int i = result;
            return i;
        }).thenApply(result -> {
            // 这里不会执行,因为上面出现了异常
            String words = "现在是" + result + "点钟";
            return words;
        }).exceptionally(error -> {
            // 我们选择在这里打印出异常
            error.printStackTrace();
            // 并且当异常发生的时候,我们返回一个默认的文字
            return "出错啊~";
        });
 
        exceptionally.thenAccept(System.out::println);
 
    }

最終出力

構成的非同期プログラミング

2 つの completableFuture を結合する

上で、Future にはそれができないと述べたことを覚えていますか?

  • 2 つの非同期計算を 1 つに結合します。2 つの非同期計算は互いに独立しており、2 番目の非同期計算は最初の非同期計算の結果に依存します。

thenApply()

シナリオを想定すると、私は小学生で、今日は何科目受講する必要があるかを知りたいのですが、このとき、 1. 私の名前に従って生徒情報を取得する 2. 名前に従ってコースをクエリする という 2 つのステップが必要です。私の学生情報を使用できます。 次のメソッドを使用して API をチェーンで呼び出し、前のステップの結果を使用して次のステップに進みます。

CompletableFuture<List<Lesson>> future = CompletableFuture.supplyAsync(() -> {
            // 根据学生姓名获取学生信息
            return StudentService.getStudent(name);
        }).thenApply(student -> {
            // 再根据学生信息获取今天的课程
            return LessonsService.getLessons(student);
        });

 

学生の名前から学生情報を取得し、取得した学生情報 Student を使用して apply () メソッドに渡し、学生の今日のコースリストを取得します。

  • 2 つの非同期計算を 1 つにマージします。これら 2 つの非同期計算は互いに独立しており、互いに独立しています。

thenCompose()

シナリオを想定すると、私は小学生で、今日は労働技能と美術の授業があり、今日学校に何を持っていく必要があるかを調べる必要があります。

CompletableFuture<List<String>> total = CompletableFuture.supplyAsync(() -> {
            // 第一个任务获取美术课需要带的东西,返回一个list
            List<String> stuff = new ArrayList<>();
            stuff.add("画笔");
            stuff.add("颜料");
            return stuff;
        }).thenCompose(list -> {
            // 向第二个任务传递参数list(上一个任务美术课所需的东西list)
            CompletableFuture<List<String>> insideFuture = CompletableFuture.supplyAsync(() -> {
                List<String> stuff = new ArrayList<>();
                // 第二个任务获取劳技课所需的工具
                stuff.add("剪刀");
                stuff.add("折纸");
                // 合并两个list,获取课程所需所有工具
                List<String> allStuff = Stream.of(list, stuff).flatMap(Collection::stream).collect(Collectors.toList());
                return allStuff;
            });
            return insideFuture;
        });
        System.out.println(total.join().size());

CompletableFuture.supplyAsync()メソッドを通じて 最初のタスクを作成し、アート クラスに必要な項目のリストを取得します。次に、 thenCompose() インターフェイスを使用してそのリストを 2 番目のタスクに渡し、2 番目のタスクが項目を取得します。レイバー クラスに必要であり、統合後に返されます。これまでのところ、2 つのタスクの統合は完了しています。(正直に言うと、このビジネス シナリオを実現するために Compose を使用するのは少しぎこちないように思えます。次の例を見てみましょう)

  • 2 つの非同期計算を 1 つにマージします。これら 2 つの非同期計算は互いに独立しており、互いに独立しています。

次に結合()

まだ上のシーンでは、私は小学生で、今日は労働技能の授業と美術の授業があり、今日学校に何を持っていく必要があるかを調べる必要があります。

CompletableFuture<List<String>> painting = CompletableFuture.supplyAsync(() -> {
            // 第一个任务获取美术课需要带的东西,返回一个list
            List<String> stuff = new ArrayList<>();
            stuff.add("画笔");
            stuff.add("颜料");
            return stuff;
        });
        CompletableFuture<List<String>> handWork = CompletableFuture.supplyAsync(() -> {
            // 第二个任务获取劳技课需要带的东西,返回一个list
            List<String> stuff = new ArrayList<>();
            stuff.add("剪刀");
            stuff.add("折纸");
            return stuff;
        });
        CompletableFuture<List<String>> total = painting
                // 传入handWork列表,然后得到两个CompletableFuture的参数Stuff1和2
                .thenCombine(handWork, (stuff1, stuff2) -> {
                    // 合并成新的list
                    List<String> totalStuff = Stream.of(stuff1, stuff1)
                            .flatMap(Collection::stream)
                            .collect(Collectors.toList());
                    return totalStuff;
                });
        System.out.println(JSONObject.toJSONString(total.join()));
  • Future コレクション内のすべてのタスクが完了するまで待ちます。

完了した結果をすべて取得 - allOf

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)

allOf メソッドは、指定されたすべてのタスクが完了したときに、まったく新しい完了した CompletableFuture を返します。

CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            try {
                //使用sleep()模拟耗时操作
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 1;
        });
 
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            return 2;
        });
        CompletableFuture.allOf(future1, future1);
        // 输出3
        System.out.println(future1.join()+future2.join());
最初に完了したタスクの結果を取得します - anyOf
  • Future コレクション内の最も速く終了するタスクが完了するのを待って (おそらく、同じ値を別の方法で計算しようとしたため)、その結果を返します。 ヒント : 最も速く完了したタスクに例外がある場合は、例外が最初に返されます。間違いを恐れる場合は、Exceptionally()を追加して 、起こり得る例外を処理し、デフォルトの戻り値を設定できます。
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            throw new NullPointerException();
        });
 
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            try {
                // 睡眠3s模拟延时
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 1;
        });
        CompletableFuture<Object> anyOf = CompletableFuture
                .anyOf(future, future2)
                .exceptionally(error -> {
                    error.printStackTrace();
                    return 2;
                });
        System.out.println(anyOf.join());

いくつかの小さな例

複数の方法の組み合わせ

  • Future タスクの実行をプログラムで完了します (つまり、非同期操作の結果を手動で設定します)。
  • Future の完了時刻に応答します (つまり、Future の完了時刻が完了すると通知され、単に結果をブロックするだけでなく、Future の計算結果を使用して次の操作を実行できます)。手術を待っています)
public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> 1)
                .whenComplete((result, error) -> {
                    System.out.println(result);
                    error.printStackTrace();
                })
                .handle((result, error) -> {
                    error.printStackTrace();
                    return error;
                })
                .thenApply(Object::toString)
                .thenApply(Integer::valueOf)
                .thenAccept((param) -> System.out.println("done"));
    }

ループ内で同時タスクを作成する

public static void main(String[] args) {
        long begin = System.currentTimeMillis();
        // 自定义一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 循环创建10个CompletableFuture
        List<CompletableFuture<Integer>> collect = IntStream.range(1, 10).mapToObj(i -> {
            CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
                // 在i=5的时候抛出一个NPE
                if (i == 5) {
                    throw new NullPointerException();
                }
                try {
                    // 每个依次睡眠1-9s,模拟线程耗时
                    TimeUnit.SECONDS.sleep(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
                return i;
            }, executorService)
                    // 这里处理一下i=5时出现的NPE
                    // 如果这里不处理异常,那么异常会在所有任务完成后抛出,小伙伴可自行测试
                    .exceptionally(Error -> {
                        try {
                            TimeUnit.SECONDS.sleep(5);
                            System.out.println(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        return 100;
                    });
            return future;
        }).collect(Collectors.toList());
        // List列表转成CompletableFuture的Array数组,使其可以作为allOf()的参数
        // 使用join()方法使得主线程阻塞,并等待所有并行线程完成
        CompletableFuture.allOf(collect.toArray(new CompletableFuture[]{})).join();
        System.out.println("最终耗时" + (System.currentTimeMillis() - begin) + "毫秒");
        executorService.shutdown();
    }

CompletableFuture シナリオの使用

  • 時間のかかる操作、特に 1 つ以上のリモート サービスに依存する操作を実行する場合、非同期タスクを使用すると、プログラムのパフォーマンスが向上し、プログラムの応答が高速化されます。
  • CompletableFuture クラスを使用すると、例外管理メカニズムが提供され、非同期タスクの実行中に発生する例外をスローして管理できるようになります。
  • これらの非同期タスクが互いに独立している場合、またはそれらの一部の結果が他のタスクの入力である場合、これらの非同期タスクを 1 つに構築または結合できます。

ヒント : JUnit はメインスレッドの完了後に JVM を閉じるため、マルチスレッド テストには JUit 単体テストを使用しないでください。

おすすめ

転載: blog.csdn.net/weixin_42218169/article/details/131192865