Asynchronous programming tool: how does completable future operate?

Time:2021-11-23

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

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

}

Operation results:

The total time is 806ms
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");

}

}
It can be found that the code is much simpler using completable future. The supplyasyncwww.cungun.com 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
0c2266ac4dec5f69a317e54fb8c99831.jpeg

Create asynchronous task
Completable future creates asynchronous tasks. Generally, there are two methods: supplyasync and runasync

319a725a11f165a2abf16bca4cbe3deb.jpeg create asynchronous task

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)
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)
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: small boy picking up snail"), executor);
    //Use of supplyasync
    CompletableFuture<String> supplyFuture = CompletableFuture.supplyAsync(() -> {
                System.out.print ("supply, official account: little boy picking up snail");
                Return "the little boy who picked up 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, pay attention to the official account: the little boy picking up the snail.
null
Supply, pay attention to the official account: little boy picking up snail and picking up snail.

Task asynchronous callback
f2c5d4dcf61128632a330b068085e567.jpeg

  1. thenRun/thenRunAsync
    public CompletableFuture<Void> thenRun(Runnable action);
    public CompletableFuture<Void> thenRunAsync(Runnable action);
    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 completable future method task first");
                Return "the little boy who picked up snails";
            }
    );

    CompletableFuture thenRunFuture = orgFuture.thenRun(() -> {
        System. Out. Println ("then perform the second task");
    });

    System.out.println(thenRunFuture.get());
}

}
//Output
First execute the first completable future method task
Then perform the second task
null
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);
}

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 completabilefuture 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 used asgameThe input parameter is passed to the callback method, but the callback method has no return value.

public class FutureThenAcceptTest {

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

    CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
            ()->{
                System. Out. Println ("original completable future method task");
                Return "the little boy who picked up snails";
            }
    );

    CompletableFuture thenAcceptFuture = orgFuture.thenAccept((a) -> {
        If ("little boy picking snails". Equals (a)){
            System.out.println ("paid attention");
        }

        System.out.println ("consider first");
    });

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

}

  1. 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 completable future method task");
                Return "the little boy who picked up snails";
            }
    );

    CompletableFuture<String> thenApplyFuture = orgFuture.thenApply((a) -> {
        If ("little boy picking snails". Equals (a)){
            Return "followed";
        }

        Return "think first";
    });

    System.out.println(thenApplyFuture.get());
}

}
//Output
Original completabilefuture method task
Attention

  1. 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

  1. 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 "the little boy who picked up snails";
            }
    );

    CompletableFuture<String> rstFuture = orgFuture.whenComplete((a, throwable) -> {
        System. Out. Println ("current thread Name:" + thread. Currentthread(). Getname());
        System. Out. Println ("pass" + A + "after the last task is completed");
        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

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

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

        System. Out. Println ("pass" + A + "after the last task is completed");
       
        }
        System.out.println("233333");
        return null;
    });

    System.out.println(rstFuture.get());
}

}

Current thread Name: forkjoinpool.commonpool-worker-1
When the last task was finished, I passed the little boy who picked up snails
666
Attention
Multiple task combination processing
ef1d149746dc30593932c4e25ff26e04.jpeg

And combination relationship
47611ce13f4e4ec9b86377f869ff45af.jpeg

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 {

    Completabilefuture < string > first = completabilefuture.completedfuture ("the 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 "the 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
Or combination relationship
3a8b9d26a3142a217cb5f53255d06502.jpeg

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 ("finish the first asynchronous task");}
            catch (Exception e){
                Return "the first task is abnormal";
            }
        Return "the first asynchronous task";
    });
    ExecutorService executor = Executors.newSingleThreadExecutor();
    CompletableFuture<Void> future = CompletableFuture
            //Second asynchronous task
            .supplyAsync(() -> {
                        System. Out. Println ("finish the second task");
                        Return "the second task";}
            , executor)
            //Third task
            .acceptEitherAsync(first, System.out::println, executor);

    executor.shutdown();
}

}
//Output
Finish the second task
Second task
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 have finished executing");
    });
    CompletableFuture<Void> b = CompletableFuture.runAsync(() -> {
        System. Out. Println ("I'm finished too");
    });
    CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(a, b).whenComplete((m,k)->{
        System.out.println("finish");
    });
}

}
//Output
I’m done
I’m done, too
finish
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 have finished executing");
    });
    CompletableFuture<Void> b = CompletableFuture.runAsync(() -> {
        System. Out. Println ("I'm finished 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
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 {

    Completable future < string > F = completable future.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
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.

db4f4125408776ec1224fa1ad1d52a87.jpeg

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

  1. Considerations 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.
  2. 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.

Recommended Today

Application of observer pattern in design pattern in Ruby Programming

Observer mode(sometimes referred to as publish / subscribe mode) is softwareDesign patternA kind of.In this mode, a target object manages all its dependent observer objects and actively notifies when its own state changes.This is usually achieved by calling the methods provided by each observer. When implementing the observer mode, it should be noted that the […]