マルチスレッドと同時実行 - Java 8 CompletableFuture 非同期マルチスレッド

1. Future のレビュー例

一部のビジネス シナリオでは、タスクの実行を高速化するために、マルチスレッドを使用してタスクを非同期に実行する必要があります。

JDK5 には、非同期計算の結果を記述するために使用される Future インターフェイスが追加されました。

Future および関連する使用メソッドはタスクを非同期に実行する機能を提供しますが、結果を取得するのは非常に不便です。Future.get() を使用して呼び出しスレッドをブロックするか、ポーリングを使用して Future.isDone タスクが When であるかどうかを判断する必要があります。完了したら、再度結果を取得します。

これら 2 つの処理方法はどちらもあまり洗練されていません。関連するコードは次のとおりです。

    @Test
    public void testFuture() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        Future<String> future = executorService.submit(() -> {
            Thread.sleep(2000);
            return "hello";
        });
        System.out.println(future.get());
        System.out.println("end");
    }

同時に、Future では、複数の非同期タスクが相互に依存する必要があるシナリオを解決できません。簡単に言うと、メインスレッドは、サブスレッドのタスクが実行されるのを待ってから実行する必要があります。 "CountDownLatch" を思い浮かべるかもしれませんが、これは true です。解決されたコードは次のとおりです。

ここでは 2 つの Future が定義されています。1 つ目はユーザー ID を通じてユーザー情報を取得し、2 つ目は製品 ID を通じて製品情報を取得します。

    @Test
    public void testCountDownLatch() throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        CountDownLatch downLatch = new CountDownLatch(2);
        long startTime = System.currentTimeMillis();
        Future<String> userFuture = executorService.submit(() -> {
            //模拟查询商品耗时500毫秒
            Thread.sleep(500);
            downLatch.countDown();
            return "用户A";
        });

        Future<String> goodsFuture = executorService.submit(() -> {
            //模拟查询商品耗时500毫秒
            Thread.sleep(400);
            downLatch.countDown();
            return "商品A";
        });

        downLatch.await();
        //模拟主程序耗时时间
        Thread.sleep(600);
        System.out.println("获取用户信息:" + userFuture.get());
        System.out.println("获取商品信息:" + goodsFuture.get());
        System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");

    }

「操作結果」

获取用户信息:用户A
获取商品信息:商品A
总共用时1110ms

実行結果から、結果が得られていることがわかります。非同期操作を使用しない場合、実行時間は 500+400+600 = 1500 となり、非同期操作を使用した後の実際のコストはわずかです。 1110。

しかし、Java8 以降は、これがエレガントな解決策であるとは思えなくなりました。次に、CompletableFuture の使用法を理解しましょう。

2. CompletableFuture を通じて上記の例を実装します。

    @Test
    public void testCompletableInfo() throws InterruptedException, ExecutionException {
        long startTime = System.currentTimeMillis();

        //调用用户服务获取用户基本信息
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() ->
                //模拟查询商品耗时500毫秒
        {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "用户A";
        });

        //调用商品服务获取商品基本信息
        CompletableFuture<String> goodsFuture = CompletableFuture.supplyAsync(() ->
                //模拟查询商品耗时500毫秒
        {
            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "商品A";
        });

        System.out.println("获取用户信息:" + userFuture.get());
        System.out.println("获取商品信息:" + goodsFuture.get());

        //模拟主程序耗时时间
        Thread.sleep(600);
        System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");
    }

演算結果

获取用户信息:用户A
获取商品信息:商品A
总共用时1112ms

CountDownLatch の機能は CompletableFuture を通じて簡単に実現できます。これで終わりだと思いますが、それよりも CompletableFuture の方がはるかに強力です。

たとえば、タスク 1 の実行後にタスク 2 が実行され、タスク 1 の実行結果もタスク 2 の入力パラメータとして使用されるなど、強力な関数を実現できます。CompletableFuture の API を学びましょう。

3. CompletableFutureの作成方法

3.1. 一般的に使用される 4 つの作成方法

CompletableFuture ソース コードには、非同期タスクを実行するための 4 つの静的メソッドがあります。

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier){..}
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor){..}
public static CompletableFuture<Void> runAsync(Runnable runnable){..}
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor){..}

一般に、上記の静的メソッドを使用して CompletableFuture を作成します。ここでは、それらの違いについて説明します。

  • 「supplyAsync」はタスクを実行し、戻り値をサポートします。

  • 「runAsync」は値を返さずにタスクを実行します。

3.1.1、「supplyAsync方法」

//使用默认内置线程池ForkJoinPool.commonPool(),根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
//自定义线程,根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

3.1.2、「runAsync メソッド」

//使用默认内置线程池ForkJoinPool.commonPool(),根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable) 
//自定义线程,根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable,  Executor executor)

3.2. 結果を得る 4 つの方法

CompltableFuture クラスは、結果を取得する 4 つの方法を提供します。

//方式一
public T get()
//方式二
public T get(long timeout, TimeUnit unit)
//方式三
public T getNow(T valueIfAbsent)
//方式四
public T join()

例証します:

  • 「get()とget(long timeout, TimeUnit単位)」  => Futureではすでに提供されており、後者はタイムアウト処理を提供し、指定時間内に結果が得られない場合はタイムアウト例外がスローされます

  • "getNow"  => ブロックせずにすぐに結果を取得します。結果の計算が完了した場合は結果を返すか、計算処理の例外を返します。計算が完了していない場合は、設定された valueIfAbsent 値を返します

  • "join"  => メソッド内で例外はスローされません

示例

    @Test
    public void testCompletableGet() throws InterruptedException, ExecutionException {

        CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "商品A";
        });

        // getNow方法测试 
        System.out.println(cp1.getNow("商品B"));

        //join方法测试 
        CompletableFuture<Integer> cp2 = CompletableFuture.supplyAsync((() -> 1 / 0));
        System.out.println(cp2.join());
        System.out.println("-----------------------------------------------------");
        //get方法测试
        CompletableFuture<Integer> cp3 = CompletableFuture.supplyAsync((() -> 1 / 0));
        System.out.println(cp3.get());
    }

「操作結果」:

  • 最初の実行結果は 「製品B」ですが、1秒スリープする必要があるため、すぐには結果が得られません

  • join メソッドは結果を取得するメソッドで例外をスローしませんが、実行結果は例外をスローします。スローされる例外は CompletionException です。

  • resultメソッドを取得するgetメソッドでは例外がスローされ、実行結果によってスローされる例外はExecutionExceptionです。

4. 非同期コールバックメソッド

4.1、その後実行/次に非同期実行

平たく言えば、「最初のタスクが終了したら、2 番目のタスクを実行します。2 番目のタスクには戻り値がありません。 」

    @Test
    public void testCompletableThenRunAsync() throws InterruptedException, ExecutionException {
        long startTime = System.currentTimeMillis();

        CompletableFuture<Void> cp1 = CompletableFuture.runAsync(() -> {
            try {
                //执行任务A
                Thread.sleep(600);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });

        CompletableFuture<Void> cp2 = cp1.thenRun(() -> {
            try {
                //执行任务B
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // get方法测试
        System.out.println(cp2.get());

        //模拟主程序耗时时间
        Thread.sleep(600);
        System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");
    }

    //运行结果
    /**
     *  null
     *  总共用时1610ms
     */

「thenRun と thenRunAsync の違いは何ですか?」

最初のタスクを実行するときにカスタム スレッド プールを渡す場合:

  • thenRun メソッドが呼び出されて 2 番目のタスクが実行されると、2 番目のタスクと最初のタスクは同じスレッド プールを共有します。

  • thenRunAsync を呼び出して 2 番目のタスクを実行する場合、最初のタスクは自分で渡されたスレッド プールを使用し、2 番目のタスクは ForkJoin スレッド プールを使用します。

说明: thenAccept と thenAcceptAsync、後ほど紹介する thenApply と thenApplyAsync の違いも同様です。

4.2、thenAccept/thenAcceptAsync

最初のタスクが実行された後、2 番目のコールバック メソッドのタスクが実行され、タスクの実行結果が入力パラメータとしてコールバック メソッドに渡されますが、コールバック メソッドには戻り値がありません。

示例

    @Test
    public void testCompletableThenAccept() throws ExecutionException, InterruptedException {
        long startTime = System.currentTimeMillis();
        CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {
            return "dev";

        });
        CompletableFuture<Void> cp2 = cp1.thenAccept((a) -> {
            System.out.println("上一个任务的返回结果为: " + a);
        });

        cp2.get();
    }

4.3、 thenApply/thenApplyAsync

最初のタスクが実行された後、2 番目のコールバック メソッドのタスクが実行され、タスクの実行結果が入力パラメータとしてコールバック メソッドに渡され、コールバック メソッドが戻り値を持つことを示します。

示例

    @Test
    public void testCompletableThenApply() throws ExecutionException, InterruptedException {
        CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {
            return "dev";

        }).thenApply((a) -> {
            if (Objects.equals(a, "dev")) {
                return "dev";
            }
            return "prod";
        });

        System.out.println("当前环境为:" + cp1.get());

        //输出: 当前环境为:dev
    }

5. 異常なコールバック

CompletableFuture のタスクが正常に完了するか例外が発生すると、 「whenComplete」コールバック関数が呼び出されます。

  • 「正常完了」 : whenComplete が上位タスクと同じ結果を返し、例外は null です。

  • "Exception" : Complete が null を返し、例外が上位タスクの例外である場合。

つまり、get()を呼び出した場合、正常に終了した場合は結果が得られ、例外が発生した場合は例外がスローされるため、その例外をハンドリングする必要があります。

例を見てみましょう

5.1、whenComplete のみを使用する

    @Test
    public void testCompletableWhenComplete() throws ExecutionException, InterruptedException {
        CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {

            if (Math.random() < 0.5) {
                throw new RuntimeException("出错了");
            }
            System.out.println("正常结束");
            return 0.11;

        }).whenComplete((aDouble, throwable) -> {
            if (aDouble == null) {
                System.out.println("whenComplete aDouble is null");
            } else {
                System.out.println("whenComplete aDouble is " + aDouble);
            }
            if (throwable == null) {
                System.out.println("whenComplete throwable is null");
            } else {
                System.out.println("whenComplete throwable is " + throwable.getMessage());
            }
        });
        System.out.println("最终返回的结果 = " + future.get());
    }

例外なく正常に完了した場合:

正常结束
whenComplete aDouble is 0.11
whenComplete throwable is null
最终返回的结果 = 0.11

例外が発生した場合: get() は例外をスローします。

whenComplete aDouble is null
whenComplete throwable is java.lang.RuntimeException: 出错了

java.util.concurrent.ExecutionException: java.lang.RuntimeException: 出错了
 at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
 at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)

5.2、whenComplete + 例外的な例

    @Test
    public void testWhenCompleteExceptionally() throws ExecutionException, InterruptedException {
        CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("出错了");
            }
            System.out.println("正常结束");
            return 0.11;

        }).whenComplete((aDouble, throwable) -> {
            if (aDouble == null) {
                System.out.println("whenComplete aDouble is null");
            } else {
                System.out.println("whenComplete aDouble is " + aDouble);
            }
            if (throwable == null) {
                System.out.println("whenComplete throwable is null");
            } else {
                System.out.println("whenComplete throwable is " + throwable.getMessage());
            }
        }).exceptionally((throwable) -> {
            System.out.println("exceptionally中异常:" + throwable.getMessage());
            return 0.0;
        });

        System.out.println("最终返回的结果 = " + future.get());
    }

例外が発生した場合、その例外は例外的に捕捉され、デフォルトの戻り値 0.0 が返されます。

whenComplete aDouble is null
whenComplete throwable is java.lang.RuntimeException: 出错了
exceptionally中异常:java.lang.RuntimeException: 出错了
最终返回的结果 = 0.0

6. マルチタスクの組み合わせのコールバック

6.1、AND結合関係

thenCombine / thenAccept Both / runAfter Both はすべて、「タスク 1 とタスク 2 が完了したら、タスク 3 を実行する」という意味です。

違いは次のとおりです。

  • 「runAfter Both」は 実行結果をメソッドの入力パラメータとして使用せず、戻り値を持ちません。

  • "thenAccept Both" : 2 つのタスクの実行結果は、メソッド入力パラメーターとして指定されたメソッドに渡され、戻り値はありません。

  • "thenCombine" : 2 つのタスクの実行結果はメソッドの入力パラメータとして使用され、指定されたメソッドに渡され、戻り値が返されます。

    @Test
    public void testCompletableThenCombine() throws ExecutionException, InterruptedException {
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //开启异步任务1
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            System.out.println("异步任务1,当前线程是:" + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("异步任务1结束");
            return result;
        }, executorService);

        //开启异步任务2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("异步任务2,当前线程是:" + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("异步任务2结束");
            return result;
        }, executorService);

        //任务组合
        CompletableFuture<Integer> task3 = task.thenCombineAsync(task2, (f1, f2) -> {
            System.out.println("执行任务3,当前线程是:" + Thread.currentThread().getId());
            System.out.println("任务1返回值:" + f1);
            System.out.println("任务2返回值:" + f2);
            return f1 + f2;
        }, executorService);

        Integer res = task3.get();
        System.out.println("最终结果:" + res);
    }

「操作結果」

异步任务1,当前线程是:17
异步任务1结束
异步任务2,当前线程是:18
异步任务2结束
执行任务3,当前线程是:19
任务1返回值:2
任务2返回值:2
最终结果:4

6.2、OR結合関係

applyToEither / acceptEither / runAfterEither はすべて、「2 つのタスク、1 つのタスクが完了している限り、3 つのタスクが実行される」という意味です。

違いは次のとおりです。

  • "runAfterEither" : 実行結果はメソッドの入力パラメータとして使用されず、戻り値はありません

  • "acceptEither" : 実行されたタスクはメソッド入力パラメータとして指定されたメソッドに渡され、戻り値はありません

  • "applyToEither" : 実行されたタスクはメソッド入力パラメータとして指定されたメソッドに渡され、戻り値が返されます。

示例

    @Test
    public void testCompletableEitherAsync() {
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //开启异步任务1
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            System.out.println("异步任务1,当前线程是:" + Thread.currentThread().getId());

            int result = 1 + 1;
            System.out.println("异步任务1结束");
            return result;
        }, executorService);

        //开启异步任务2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("异步任务2,当前线程是:" + Thread.currentThread().getId());
            int result = 1 + 2;
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("异步任务2结束");
            return result;
        }, executorService);

        //任务组合
        task.acceptEitherAsync(task2, (res) -> {
            System.out.println("执行任务3,当前线程是:" + Thread.currentThread().getId());
            System.out.println("上一个任务的结果为:" + res);
        }, executorService);
    }

演算結果

//通过结果可以看出,异步任务2都没有执行结束,任务3获取的也是1的执行结果
异步任务1,当前线程是:17
异步任务1结束
异步任务2,当前线程是:18
执行任务3,当前线程是:19
上一个任务的结果为:2

知らせ

上記のコアスレッドの数を 1 に変更すると、

 ExecutorService executorService = Executors.newFixedThreadPool(1);

実行結果は以下の通りですが、タスク3は全く実行されておらず、当然タスク3はそのまま破棄されています。

异步任务1,当前线程是:17
异步任务1结束
异步任务2,当前线程是:17

6.3、マルチタスクの組み合わせ

  • "allOf" : すべてのタスクが完了するまで待ちます

  • "anyOf" : 完了したタスクが 1 つある限り

allOf: すべてのタスクが完了するまで待ちます

    @Test
    public void testCompletableAallOf() throws ExecutionException, InterruptedException {
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //开启异步任务1
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            System.out.println("异步任务1,当前线程是:" + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("异步任务1结束");
            return result;
        }, executorService);

        //开启异步任务2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("异步任务2,当前线程是:" + Thread.currentThread().getId());
            int result = 1 + 2;
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("异步任务2结束");
            return result;
        }, executorService);

        //开启异步任务3
        CompletableFuture<Integer> task3 = CompletableFuture.supplyAsync(() -> {
            System.out.println("异步任务3,当前线程是:" + Thread.currentThread().getId());
            int result = 1 + 3;
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("异步任务3结束");
            return result;
        }, executorService);

        //任务组合
        CompletableFuture<Void> allOf = CompletableFuture.allOf(task, task2, task3);

        //等待所有任务完成
        allOf.get();
        //获取任务的返回结果
        System.out.println("task结果为:" + task.get());
        System.out.println("task2结果为:" + task2.get());
        System.out.println("task3结果为:" + task3.get());
    }

anyOf: 完了したタスクが 1 つある限り

    @Test
    public void testCompletableAnyOf() throws ExecutionException, InterruptedException {
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //开启异步任务1
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            int result = 1 + 1;
            return result;
        }, executorService);

        //开启异步任务2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            int result = 1 + 2;
            return result;
        }, executorService);

        //开启异步任务3
        CompletableFuture<Integer> task3 = CompletableFuture.supplyAsync(() -> {
            int result = 1 + 3;
            return result;
        }, executorService);

        //任务组合
        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(task, task2, task3);
        //只要有一个有任务完成
        Object o = anyOf.get();
        System.out.println("完成的任务的结果:" + o);
    }

7. CompletableFutureを利用する際の注意点は何ですか?

 CompletableFuture は非同期プログラミングをより便利にし、コードをより洗練させますが、それといくつかの注意点にも注意を払う必要があります。

7.1. Future は例外情報を取得するために戻り値を取得する必要があります

    @Test
    public void testWhenCompleteExceptionally() {
        CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {
            if (1 == 1) {
                throw new RuntimeException("出错了");
            }
            return 0.11;
        });

        //如果不加 get()方法这一行,看不到异常信息
        //future.get();
    }

Future は例外情報を取得するために戻り値を取得する必要があります。get()/join() メソッドを追加しない場合、例外情報は表示されません。

使用する場合は注意して、try...catch... を追加するか、例外的な方法を使用するかを検討してください。

7.2. CompletableFuture の get() メソッドがブロックされる

CompletableFuture の get() メソッドはブロッキングされているため、非同期呼び出しの戻り値を取得するために使用する場合は、タイムアウトを追加する必要があります。

//反例
 CompletableFuture.get();
//正例
CompletableFuture.get(5, TimeUnit.SECONDS);

7.3. デフォルトのスレッドプールの使用は推奨されません

CompletableFuture コードもデフォルトの「ForkJoin スレッド プール」を使用し、処理されるスレッドの数はコンピュータの「CPU コア番号 - 1」になります。大量のリクエストが来たとき、処理ロジックが複雑だとレスポンスが非常に遅くなってしまいます。一般に、カスタム スレッド プールを使用し、スレッド プール構成パラメータを最適化することをお勧めします。

7.4. スレッドプールをカスタマイズするときは、飽和戦略に注意してください

CompletableFuture の get() メソッドはブロックされているため、通常は future.get(5, TimeUnit.SECONDS) を使用することをお勧めします。また、通常はカスタム スレッド プールを使用することをお勧めします。

ただし、スレッド プールの拒否ポリシーが DiscardPolicy または DiscardOldestPolicy の場合、スレッド プールが飽和するとタスクは直接破棄され、例外は破棄されません。したがって、CompletableFuture スレッド プール ポリシーでは AbortPolicy を使用することが最適であり、その後、時間のかかる非同期スレッドをスレッド プールから分離する必要があります。

8. 推奨読書

CompletionService の使用法とソースコード分析

CompletableFutureの詳しい使い方

おすすめ

転載: blog.csdn.net/qq_34272760/article/details/126454737