Multithreading and Concurrency - Java 8 CompletableFuture Asynchronous Multithreading

1. An example review of Future

In some business scenarios, we need to use multithreading to execute tasks asynchronously to speed up task execution.

JDK5 has added the Future interface, which is used to describe the result of an asynchronous calculation.

Although Future and related usage methods provide the ability to execute tasks asynchronously, it is very inconvenient to obtain the results. We must use Future.get() to block the calling thread, or use polling to determine whether the Future.isDone task is When finished, get the result again.

Neither of these two processing methods is very elegant, and the relevant code is as follows:

    @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");
    }

At the same time, Future cannot solve the scenario where multiple asynchronous tasks need to depend on each other. To put it simply, the main thread needs to wait for the sub-thread tasks to be executed before executing them. At this time, you may think of "CountDownLatch", which is true. Solved, the code is as follows.

Two Futures are defined here, the first obtains user information through the user id, and the second obtains product information through the product 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");

    }

"operation result"

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

From the running results, it can be seen that the results have been obtained, and if we do not use asynchronous operations, the execution time should be: 500+400+600 = 1500, and after using asynchronous operations, the actual cost is only 1110.

But after Java8, I no longer think this is an elegant solution. Next, let's understand the use of CompletableFuture.

2. Implement the above example through 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");
    }

operation result

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

The function of CountDownLatch can be easily realized through CompletableFuture, you think this is over, far more than that, CompletableFuture is much stronger than this.

For example, it can be realized: task 2 is executed after task 1 is executed, and even the result of task 1 execution is used as an input parameter of task 2 and other powerful functions. Let's learn the API of CompletableFuture.

3. CompletableFuture creation method

3.1. Four commonly used creation methods

There are four static methods in CompletableFuture source code to perform asynchronous tasks

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){..}

Generally, we use the above static method to create CompletableFuture, and here is an explanation of their differences:

  • "supplyAsync" executes tasks and supports return values.

  • "runAsync" executes the task without returning a value.

3.1.1, "supplyAsync Method"

//使用默认内置线程池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 method"

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

3.2. 4 ways to obtain results

The CompltableFuture class provides four ways to obtain the results

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

illustrate:

  • "get() and get(long timeout, TimeUnit unit)"  => already provided in Future, the latter provides timeout processing, if the result is not obtained within the specified time, a timeout exception will be thrown

  • "getNow"  => Immediately get the result without blocking. If the calculation of the result is completed, it will return the result or an exception in the calculation process. If the calculation is not completed, it will return the set valueIfAbsent value

  • "join"  => No exception will be thrown in the method

示例

    @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());
    }

"operation result":

  • The first execution result is  "product B" , because it needs to sleep for 1 second, the result cannot be obtained immediately

  • The join method will not throw an exception in the method to obtain the result, but the execution result will throw an exception, and the thrown exception is CompletionException

  • An exception will be thrown in the get method to obtain the result method, and the exception thrown by the execution result is ExecutionException

4. Asynchronous callback method

4.1、thenRun/thenRunAsync

In layman's terms, "After finishing the first task, do the second task, and the second task has no return value . "

example

    @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
     */

"What's the difference between thenRun and thenRunAsync?"

If you pass in a custom thread pool when you execute the first task:

  • When the thenRun method is called to execute the second task, the second task and the first task share the same thread pool.

  • When calling thenRunAsync to execute the second task, the first task uses the thread pool passed in by yourself, and the second task uses the ForkJoin thread pool.

说明: The difference between thenAccept and thenAcceptAsync, thenApply and thenApplyAsync introduced later is also the same.

4.2、thenAccept/thenAcceptAsync

After the first task is executed, the second callback method task is executed, and the execution result of the task will be passed to the callback method as an input parameter, but the callback method has no return value.

示例

    @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

Indicates that after the first task is executed, the second callback method task is executed, and the execution result of the task will be passed to the callback method as an input parameter, and the callback method has a return value.

示例

    @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. Abnormal callback

When the CompletableFuture's task is completed normally or an exception occurs, it will call the "whenComplete" callback function.

  • "Normal completion" : whenComplete returns the same result as the superior task, and the exception is null;

  • "Exception" : whenComplete returns null, and the exception is the exception of the superior task;

That is, when get() is called, the result will be obtained when it is completed normally, and an exception will be thrown when an exception occurs, and you need to handle the exception.

Let's see an example

5.1, only use 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());
    }

When completed normally, without exception:

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

When an exception occurs: get() will throw an exception

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 + exceptionally example

    @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());
    }

When an exception occurs, the exception will be caught in exceptionally, giving a default return value of 0.0.

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

6. Multi-task combination callback

6.1, AND combination relationship

thenCombine / thenAcceptBoth / runAfterBoth all mean: "When task 1 and task 2 are completed, then execute task 3" .

The difference is that:

  • "runAfterBoth"  will not use the execution result as a method input parameter, and has no return value

  • "thenAcceptBoth" : The execution results of the two tasks will be passed to the specified method as method input parameters, and there will be no return value

  • "thenCombine" : The execution results of the two tasks will be used as method input parameters, passed to the specified method, and have a return value

example

    @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);
    }

"operation result"

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

6.2, OR combination relationship

applyToEither / acceptEither / runAfterEither all mean: "Two tasks, as long as one task is completed, the task three will be executed" .

The difference is that:

  • "runAfterEither" : The execution result will not be used as a method input parameter, and there is no return value

  • "acceptEither" : The task that has been executed will be passed to the specified method as a method input parameter, and there will be no return value

  • "applyToEither" : The task that has been executed will be passed to the specified method as a method input parameter, and there will be a return value

示例

    @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);
    }

operation result

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

Notice

If you change the number of core threads above to 1, that is

 ExecutorService executorService = Executors.newFixedThreadPool(1);

The running result is as follows. You will find that task 3 is not executed at all. Obviously, task 3 is directly discarded.

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

6.3, multi-task combination

  • "allOf" : wait for all tasks to complete

  • "anyOf" : As long as there is one task completed

example

allOf: wait for all tasks to complete

    @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: as long as there is one task completed

    @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. What are the points to pay attention to when using CompletableFuture?

 While CompletableFuture makes our asynchronous programming more convenient and the code more elegant, we should also pay attention to it and some points for attention.

7.1. Future needs to get the return value to get the exception information

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

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

Future needs to get the return value to get the exception information. If you do not add the get()/join() method, you will not see the exception information.

When using it, friends, pay attention to it, and consider whether to add try...catch...or use the exceptionally method.

7.2. The get() method of CompletableFuture is blocked

The get() method of CompletableFuture is blocking. If you use it to get the return value of an asynchronous call, you need to add a timeout.

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

7.3. It is not recommended to use the default thread pool

The CompletableFuture code also uses the default "ForkJoin thread pool" , and the number of threads processed is the computer's "CPU core number - 1" . When a large number of requests come in, if the processing logic is complicated, the response will be very slow. It is generally recommended to use a custom thread pool and optimize the thread pool configuration parameters.

7.4. When customizing the thread pool, pay attention to the saturation strategy

The get() method of CompletableFuture is blocking, we generally recommend using future.get(5, TimeUnit.SECONDS). And it is generally recommended to use a custom thread pool.

However, if the thread pool rejection policy is DiscardPolicy or DiscardOldestPolicy, when the thread pool is saturated, the task will be discarded directly, and the exception will not be discarded. Therefore, it is suggested that the CompletableFuture thread pool policy is best to use AbortPolicy, and then time-consuming asynchronous threads should be isolated from the thread pool.

8. Recommended reading

CompletionService usage and source code analysis

Detailed use of CompletableFuture

Guess you like

Origin blog.csdn.net/qq_34272760/article/details/126454737