Asynchronous programming weapon: a detailed explanation of completable future | Java development practice

When we execute a task asynchronously, we usually use the thread pool Executor to create it. If no return value is required, the task implements the Runnable interface; If a return value is required, the task implements the Callable interface, calls the submit method of the Executor, and then uses Future to obtain it. What should we do if multiple threads have dependency combinations? The synchronization components CountDownLatch and CyclicBarrier can be used, but they are troublesome. In fact, there is a simple method, which is to use CompeletableFuture. Recently, I just optimized the code in the project with completable Future, so I'll learn completable Future with you.

An example reviews the Future

Because completable Future implements the Future interface, let's review Future first.

Future is a new interface added to Java 5, which provides a function of asynchronous parallel computing. If the main thread needs to perform a time-consuming computing task, we can put this task into an asynchronous thread through future. The main thread continues to process other tasks. After the processing is completed, the calculation results are obtained through future.

Let's take a simple example. Suppose we have two task services, one is to query user basic information, and the other is to query user medal information. As follows,

public class UserInfoService {

    public UserInfo getUserInfo(Long userId) throws InterruptedException {
        Thread.sleep(300);//Simulation call time
        return new UserInfo("666", "Little boy picking snails", 27); //Generally, it is returned by querying the database or remote call
    }
}

public class MedalService {

    public MedalInfo getMedalInfo(long userId) throws InterruptedException {
        Thread.sleep(500); //Simulation call time
        return new MedalInfo("666", "Guardian Medal");
    }
}
Copy code

Next, let's demonstrate how to use Future to make asynchronous calls in the main thread.

public class FutureTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(10);

        UserInfoService userInfoService = new UserInfoService();
        MedalService medalService = new MedalService();
        long userId =666L;
        long startTime = System.currentTimeMillis();

        //Call user service to get basic user information
        FutureTask<UserInfo> userInfoFutureTask = new FutureTask<>(new Callable<UserInfo>() {
            @Override
            public UserInfo call() throws Exception {
                return userInfoService.getUserInfo(userId);
            }
        });
        executorService.submit(userInfoFutureTask);

        Thread.sleep(300); //Simulating other operations of the main thread takes time

        FutureTask<MedalInfo> medalInfoFutureTask = new FutureTask<>(new Callable<MedalInfo>() {
            @Override
            public MedalInfo call() throws Exception {
                return medalService.getMedalInfo(userId);
            }
        });
        executorService.submit(medalInfoFutureTask);

        UserInfo userInfo = userInfoFutureTask.get();//Get personal information results
        MedalInfo medalInfo = medalInfoFutureTask.get();//Obtain medal information results

        System.out.println("Total time" + (System.currentTimeMillis() - startTime) + "ms");
    }
}
    
Copy code

Operation results:

Total time 806 ms
 Copy code

If we do not use future to make parallel asynchronous calls, but serial calls in the main thread, the time will be about 300+500+300 = 1100 ms. It can be found that the asynchronous cooperation of future + thread pool improves the execution efficiency of the program.

However, Future is not very friendly to the acquisition of results. It can only obtain the results of tasks by blocking or polling.

  • Future.get() is a blocking call. The get method will block until the thread gets the result.
  • Future provides an isDone method, which can be polled in the program to query the execution results.

The blocking method is contrary to the design concept of asynchronous programming, and the polling method will consume unnecessary CPU resources. Therefore, JDK8 designs a completable future. Completable future provides a mechanism similar to the observer mode, which allows the listener to be notified when the task is completed.

An example walks into the completable future

Based on the above Future example, we use completable Future to implement it

public class FutureTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {

        UserInfoService userInfoService = new UserInfoService();
        MedalService medalService = new MedalService();
        long userId =666L;
        long startTime = System.currentTimeMillis();

        //Call user service to get basic user information
        CompletableFuture<UserInfo> completableUserInfoFuture = CompletableFuture.supplyAsync(() -> userInfoService.getUserInfo(userId));

        Thread.sleep(300); //Simulating other operations of the main thread takes time

        CompletableFuture<MedalInfo> completableMedalInfoFuture = CompletableFuture.supplyAsync(() -> medalService.getMedalInfo(userId)); 

        UserInfo userInfo = completableUserInfoFuture.get(2,TimeUnit.SECONDS);//Get personal information results
        MedalInfo medalInfo = completableMedalInfoFuture.get();//Obtain medal information results
        System.out.println("Total time" + (System.currentTimeMillis() - startTime) + "ms");

    }
}
Copy code

It can be found that the code is much simpler using completable future. The supplyAsync method of completable future provides the function of asynchronous execution, and the thread pool does not need to be created separately. In fact, it uses completable future. The default thread pool is ForkJoinPool.commonPool.

Completable future provides dozens of methods to assist our asynchronous task scenario. These methods include creating asynchronous tasks, asynchronous callback of tasks, combined processing of multiple tasks, etc. Let's study together

Completable future usage scenario

Create asynchronous task

Completable future creates asynchronous tasks. Generally, there are two methods: supplyAsync and runAsync

  • supplyAsync executes the completable future task and supports the return value
  • runAsync executes the completable future task without a return value.

supplyAsync method

//Use the default built-in thread pool ForkJoinPool.commonPool(), and execute tasks according to the supplier build
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
//Custom thread, and execute tasks according to supplier build
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
Copy code

runAsync method

//Use the default built-in thread pool ForkJoinPool.commonPool() to build and execute tasks according to runnable
public static CompletableFuture<Void> runAsync(Runnable runnable) 
//Customize the thread to build and execute tasks according to runnable
public static CompletableFuture<Void> runAsync(Runnable runnable,  Executor executor)
Copy code

The example code is as follows:

public class FutureTest {

    public static void main(String[] args) {
        //You can customize the thread pool
        ExecutorService executor = Executors.newCachedThreadPool();
        //Use of runAsync
        CompletableFuture<Void> runFuture = CompletableFuture.runAsync(() -> System.out.println("run,Official account:Little boy picking snails"), executor);
        //Use of supplyAsync
        CompletableFuture<String> supplyFuture = CompletableFuture.supplyAsync(() -> {
                    System.out.print("supply,Official account:Little boy picking snails");
                    return "Little boy picking snails"; }, executor);
        //The future of runAsync has no return value and outputs null
        System.out.println(runFuture.join());
        //The future of supplyAsync has a return value
        System.out.println(supplyFuture.join());
        executor.shutdown(); // The thread pool needs to be closed
    }
}
//output
run,Official account:Little boy picking snails
null
supply,Official account:Little boy picking snails little boy picking snails

Copy code

Task asynchronous callback

1. thenRun/thenRunAsync

public CompletableFuture<Void> thenRun(Runnable action);
public CompletableFuture<Void> thenRunAsync(Runnable action);
Copy code

The thenRun method of completable future, to put it mildly, is to do the second task after completing the first task. After a task is executed, execute the callback method; However, the first and second tasks have no parameters passed, and the second task has no return value

public class FutureThenRunTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
                ()->{
                    System.out.println("Execute the first one first CompletableFuture Method task");
                    return "Little boy picking snails";
                }
        );

        CompletableFuture thenRunFuture = orgFuture.thenRun(() -> {
            System.out.println("Then perform the second task");
        });

        System.out.println(thenRunFuture.get());
    }
}
//output
 Execute the first one first CompletableFuture Method task
 Then perform the second task
null
 Copy code

What's the difference between thenRun and thenRunAsync? Here's the source code:

   private static final Executor asyncPool = useCommonPool ?
        ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
        
    public CompletableFuture<Void> thenRun(Runnable action) {
        return uniRunStage(null, action);
    }

    public CompletableFuture<Void> thenRunAsync(Runnable action) {
        return uniRunStage(asyncPool, action);
    }
Copy code

If you pass in a custom thread pool when executing 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 you call thenRunAsync to execute the second task, the first task uses your own thread pool, and the second task uses the ForkJoin thread pool

TIPS: thenAccept and thenAcceptAsync, thenApply and thenApplyAsync are introduced later. This is also the difference between them.

2.thenAccept/thenAcceptAsync

The thenAccept method of completable future indicates that after the first task is completed, the second callback method task will be executed, and the execution result of the task will be passed to the callback method as an input parameter, but the callback method does not return a value.

public class FutureThenAcceptTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
                ()->{
                    System.out.println("original CompletableFuture Method task");
                    return "Little boy picking snails";
                }
        );

        CompletableFuture thenAcceptFuture = orgFuture.thenAccept((a) -> {
            if ("Little boy picking snails".equals(a)) {
                System.out.println("Attention");
            }

            System.out.println("Think about it first");
        });

        System.out.println(thenAcceptFuture.get());
    }
}
Copy code

3. thenApply/thenApplyAsync

The thenApply method of completable future indicates that after the first task is completed, the second callback method task will be 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.

public class FutureThenApplyTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
                ()->{
                    System.out.println("original CompletableFuture Method task");
                    return "Little boy picking snails";
                }
        );

        CompletableFuture<String> thenApplyFuture = orgFuture.thenApply((a) -> {
            if ("Little boy picking snails".equals(a)) {
                return "Attention";
            }

            return "Think about it first";
        });

        System.out.println(thenApplyFuture.get());
    }
}
//output
 original CompletableFuture Method task
 Attention
 Copy code

4. exceptionally

The exceptionally method of completabilefuture indicates the callback method to be executed when a task is executed abnormally; And throw an exception as a parameter and pass it to the callback method.

public class FutureExceptionTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
                ()->{
                    System.out.println("Current thread Name:" + Thread.currentThread().getName());
                    throw new RuntimeException();
                }
        );

        CompletableFuture<String> exceptionFuture = orgFuture.exceptionally((e) -> {
            e.printStackTrace();
            return "Your program is abnormal";
        });

        System.out.println(exceptionFuture.get());
    }
}
//output
 Current thread Name: ForkJoinPool.commonPool-worker-1
java.util.concurrent.CompletionException: java.lang.RuntimeException
	at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
	at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
	at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1592)
	at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.lang.RuntimeException
	at cn.eovie.future.FutureWhenTest.lambda$main$0(FutureWhenTest.java:13)
	at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
	... 5 more
 Your program is abnormal
 Copy code

5. whenComplete method

The whenComplete method of completabilefuture indicates the callback method executed after a task is completed, and there is no return value; And the result of completable future returned by the whenComplete method is the result of the previous task.

public class FutureWhenTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
                ()->{
                    System.out.println("Current thread Name:" + Thread.currentThread().getName());
                    try {
                        Thread.sleep(2000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return "Little boy picking snails";
                }
        );

        CompletableFuture<String> rstFuture = orgFuture.whenComplete((a, throwable) -> {
            System.out.println("Current thread Name:" + Thread.currentThread().getName());
            System.out.println("The last task has been completed, and the" + a + "Pass it on");
            if ("Little boy picking snails".equals(a)) {
                System.out.println("666");
            }
            System.out.println("233333");
        });

        System.out.println(rstFuture.get());
    }
}
//output
 Current thread Name: ForkJoinPool.commonPool-worker-1
 Current thread Name: ForkJoinPool.commonPool-worker-1
 When the last task was finished, I passed the little boy who picked up snails
666
233333
 Little boy picking snails
 Copy code

6. handle method

The handle method of completable future indicates that after a task is completed, the callback method will be executed, and there is a return value; And the result of completabilefuture returned by the handle method is the result of the execution of the callback method.

public class FutureHandlerTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
                ()->{
                    System.out.println("Current thread Name:" + Thread.currentThread().getName());
                    try {
                        Thread.sleep(2000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return "Little boy picking snails";
                }
        );

        CompletableFuture<String> rstFuture = orgFuture.handle((a, throwable) -> {

            System.out.println("The last task has been completed, and the" + a + "Pass it on");
            if ("Little boy picking snails".equals(a)) {
                System.out.println("666");
                return "Attention";
            }
            System.out.println("233333");
            return null;
        });

        System.out.println(rstFuture.get());
    }
}
//output
 Current thread Name: ForkJoinPool.commonPool-worker-1
 When the last task was finished, I passed the little boy who picked up snails
666
 Attention
 Copy code

Multiple task combination processing

AND combination relationship

Both thenCombine / thenAcceptBoth / runAfterBoth indicate that when two completable futures are combined, a task will not be executed until they are executed normally.

The difference is:

  • thenCombine: the execution results of the two tasks will be passed as method parameters to the specified method with return values
  • thenAcceptBoth: the execution results of the two tasks will be passed as method parameters to the specified method without return value
  • runAfterBoth does not take the execution result as a method parameter and does not return a value.
public class ThenCombineTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {

        CompletableFuture<String> first = CompletableFuture.completedFuture("First asynchronous task");
        ExecutorService executor = Executors.newFixedThreadPool(10);
        CompletableFuture<String> future = CompletableFuture
                //Second asynchronous task
                .supplyAsync(() -> "Second asynchronous task", executor)
                // (W, s) - > system. Out. Println (s) is the third task
                .thenCombineAsync(first, (s, w) -> {
                    System.out.println(w);
                    System.out.println(s);
                    return "Combination of two asynchronous tasks";
                }, executor);
        System.out.println(future.join());
        executor.shutdown();

    }
}
//output
 First asynchronous task
 Second asynchronous task
 Combination of two asynchronous tasks
 Copy code

OR combination relationship

Both applytoeither / accepteeither / runaftereither indicate that when two completable future are combined, a task will be executed as long as one of them is completed.

The difference is:

  • applyToEither: the completed task will be passed as a method parameter to the specified method with a return value
  • acceptEither: the completed task will be passed as a method parameter to the specified method without return value
  • runAfterEither: the execution result will not be entered as a method parameter, and there is no return value.
public class AcceptEitherTest {
    public static void main(String[] args) {
        //The first asynchronous task sleeps for 2 seconds to ensure that it executes late
        CompletableFuture<String> first = CompletableFuture.supplyAsync(()->{
            try{

                Thread.sleep(2000L);
                System.out.println("Complete the first asynchronous task");}
                catch (Exception e){
                    return "First task exception";
                }
            return "First asynchronous task";
        });
        ExecutorService executor = Executors.newSingleThreadExecutor();
        CompletableFuture<Void> future = CompletableFuture
                //Second asynchronous task
                .supplyAsync(() -> {
                            System.out.println("Finish the second task");
                            return "Second task";}
                , executor)
                //Third task
                .acceptEitherAsync(first, System.out::println, executor);

        executor.shutdown();
    }
}
//output
 Finish the second task
 Second task
 Copy code

AllOf

The completable future returned by allOf is executed only after all tasks are completed. If any task is abnormal, allOf's completable future will throw an exception when the get method is executed

public class allOfFutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CompletableFuture<Void> a = CompletableFuture.runAsync(()->{
            System.out.println("I'm done");
        });
        CompletableFuture<Void> b = CompletableFuture.runAsync(() -> {
            System.out.println("I'm done, too");
        });
        CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(a, b).whenComplete((m,k)->{
            System.out.println("finish");
        });
    }
}
//output
 I'm done
 I'm done, too
finish
 Copy code

AnyOf

After any task is executed, the completable future returned by anyOf is executed. If the executed task is abnormal, the completable future of anyOf will throw an exception when the get method is executed

public class AnyOfFutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CompletableFuture<Void> a = CompletableFuture.runAsync(()->{
            try {
                Thread.sleep(3000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("I'm done");
        });
        CompletableFuture<Void> b = CompletableFuture.runAsync(() -> {
            System.out.println("I'm done, too");
        });
        CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(a, b).whenComplete((m,k)->{
            System.out.println("finish");
//            return "the little boy who picked up snails";
        });
        anyOfFuture.join();
    }
}
//output
 I'm done, too
finish
 Copy code

thenCompose

Thenpose method will take the execution result of a task as a method parameter to execute the specified method after the execution of a task is completed. This method returns a new completable future instance

  • If the result of the completable future instance is not null, a new completable future instance based on the result is returned;
  • If the completabilefuture instance is null, then the new task is executed
public class ThenComposeTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CompletableFuture<String> f = CompletableFuture.completedFuture("First task");
        //Second asynchronous task
        ExecutorService executor = Executors.newSingleThreadExecutor();
        CompletableFuture<String> future = CompletableFuture
                .supplyAsync(() -> "Second task", executor)
                .thenComposeAsync(data -> {
                    System.out.println(data); return f; //Use the first task as a return
                }, executor);
        System.out.println(future.join());
        executor.shutdown();

    }
}
//output
 Second task
 First task
 Copy code

What should I pay attention to when using completable future

Completable future makes our asynchronous programming more convenient and the code more elegant. At the same time, we should also pay attention to some points for attention.

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

ExecutorService executorService = new ThreadPoolExecutor(5, 10, 5L,
    TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
      int a = 0;
      int b = 666;
      int c = b / a;
      return true;
   },executorService).thenAccept(System.out::println);
   
 //If you do not add the get() method, you will not see the exception information
 //future.get();
Copy code

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 you use it, pay attention to ha, and consider whether to add try...catch... Or use the exceptionally method.

2. The get() method of completabilefuture is blocked.

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

//Counterexample
 CompletableFuture.get();
//Positive example
CompletableFuture.get(5, TimeUnit.SECONDS);
Copy code

3. Precautions for default thread pool

The default thread pool is used in the completable future code, and the number of threads processed is the number of computer CPU cores - 1. When a large number of requests come, if the processing logic is complex, the response will be very slow. It is generally recommended to use custom thread pool to optimize thread pool configuration parameters.

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

The get() method of completable future is blocked. We generally recommend using future.get(3, TimeUnit.SECONDS). 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 recommended that the completable future thread pool strategy should preferably use AbortPolicy, and then isolate the thread pool for time-consuming asynchronous threads.


Author: a little boy picking up snails
Link: https://juejin.cn/post/6970558076642394142
Source: rare earth Nuggets
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.

Tags: Java Back-end

Posted on Mon, 08 Nov 2021 16:11:30 -0500 by woodplease