(6) Concurrent tool class completable future (asynchronous programming)

Posted by nkyoung1 on Sat, 22 Jan 2022 13:58:18 +0100

CompletableFuture

Asynchronization

Asynchronization is the basis for the implementation of the parallel scheme. More specifically, it is the basis for the implementation of the core scheme of using multithreading to optimize performance. For example, two serial operations are handed over to a newly created sub thread for execution. The main thread does not need to wait for the two tasks to complete. This is the so-called asynchronous operation

Java provides completable future in version 1.8 to support asynchronous programming. There are four main ways to create completable future objects:

//The default thread pool is used. The former has no return value, and the latter has a return value
static CompletableFuture<Void> 
  runAsync(Runnable runnable)
  
static <U> CompletableFuture<U> 
  supplyAsync(Supplier<U> supplier)
  
//Thread pool parameters can be specified  
static CompletableFuture<Void> 
  runAsync(Runnable runnable, Executor executor)
  
static <U> CompletableFuture<U> 
  supplyAsync(Supplier<U> supplier, Executor executor)  

After the completabilefuture object is created, runnable. XML is automatically executed asynchronously Run () method or supplier Get () method. For an asynchronous operation, you need to pay attention to two issues: one is when the asynchronous operation ends, and the other is how to obtain the execution results of the asynchronous operation. Because the completable Future class implements the Future interface, you can solve both problems through the Future interface. In addition, the completabilefuture class also implements the CompletionStage interface, which has more than 40 methods, which is difficult to understand

CompletionStage interface

The CompletionStage interface can clearly describe the serial relationship, AND aggregation relationship, OR aggregation relationship AND exception handling between threads.

Serial relation

In the previous article, the relationship between washing kettle and boiling water is serial. There are mainly four methods to describe the serial relationship: henApply, thenAccept, thenRun and thenpose.

  • The type of parameter fn in thenApply series functions is interface Function. The method related to CompletionStage in this interface is R apply(T t), which can receive parameters and support return values. Therefore, thenApply series methods return CompletionStage.
  • The type of parameter Consumer in thenAccept series methods is interface Consumer. The method related to CompletionStage in this interface is void accept(T t). Although this method supports parameters, it does not support return values. Therefore, thenAccept series methods return CompletionStage.
  • The parameter of action in thenRun series methods is Runnable, so action can neither receive parameters nor support return values, so what thenRun series methods return is also CompletionStage.
  • In these methods, Async represents asynchronous execution of fn, consumer or action. Among them, you should pay attention to thenpose series methods. This series of methods will create a new sub process, and the final result is the same as thenApply series.
CompletionStage<R> thenApply(fn);
CompletionStage<R> thenApplyAsync(fn);
CompletionStage<Void> thenAccept(consumer);
CompletionStage<Void> thenAcceptAsync(consumer);
CompletionStage<Void> thenRun(action);
CompletionStage<Void> thenRunAsync(action);
CompletionStage<R> thenCompose(fn);
CompletionStage<R> thenComposeAsync(fn);

Example: task 123 is executed serially. 2 depends on the execution result of 1 and 3 depends on the execution result of 2.

CompletableFuture<String> f0 = 
  CompletableFuture.supplyAsync(
    () -> "Hello World")      //1
  .thenApply(s -> s + " QQ")  //2
  .thenApply(String::toUpperCase);//3

System.out.println(f0.join());
//Output results
HELLO WORLD QQ

AND aggregation relationship

In the previous article, boiling water, taking tea AND making tea are the convergence relationship. The CompletionStage interface describes the AND aggregation relationship, mainly the interfaces of thenCombine, thenAcceptBoth AND runAfterBoth series. The differences of these interfaces also come from the differences of the three core parameters fn, consumer AND action.

CompletionStage<R> thenCombine(other, fn);
CompletionStage<R> thenCombineAsync(other, fn);
CompletionStage<Void> thenAcceptBoth(other, consumer);
CompletionStage<Void> thenAcceptBothAsync(other, consumer);
CompletionStage<Void> runAfterBoth(other, action);
CompletionStage<Void> runAfterBothAsync(other, action);

Example: use thenCombine() method to describe an And aggregation relationship. Thread f3 needs to wait for threads f1 And f2 to execute at the same time. To return to the end.

        CompletableFuture<Void> f1 =
                CompletableFuture.runAsync(()->{
                    int t = 7;
                    System.out.println(t);
                });
        CompletableFuture<Void> f2 =
                CompletableFuture.runAsync(()->{
                    int t = 5;
                    System.out.println(t);
                });
        CompletableFuture f3 = f1.thenCombine(f2,(__,x)->{
            return "end";
        });
        System.out.println(f3.join());

OR convergence relation

The CompletionStage interface describes the OR aggregation relationship, mainly the interfaces of applyToEither, accepteeither and runAfterEither series. The differences of these interfaces also come from the differences of the three core parameters fn, consumer and action.

CompletionStage applyToEither(other, fn);
CompletionStage applyToEitherAsync(other, fn);
CompletionStage acceptEither(other, consumer);
CompletionStage acceptEitherAsync(other, consumer);
CompletionStage runAfterEither(other, action);
CompletionStage runAfterEitherAsync(other, action);

Example: use the applyToEither() method to describe an or aggregation relationship. The or aggregation relationship only needs to be returned immediately after the execution of one thread in f1 or f2.

CompletableFuture<String> f1 = 
  CompletableFuture.supplyAsync(()->{
    int t = getRandom(5, 10);
    sleep(t, TimeUnit.SECONDS);
    return String.valueOf(t);
});
CompletableFuture<String> f2 = 
  CompletableFuture.supplyAsync(()->{
    int t = getRandom(5, 10);
    sleep(t, TimeUnit.SECONDS);
    return String.valueOf(t);
});
CompletableFuture<String> f3 = 
  f1.applyToEither(f2,s -> s);
System.out.println(f3.join());

exception handling

Although the core methods of fn, consumer and action mentioned above are not allowed to throw checkable exceptions, they cannot be restricted to throw runtime exceptions. The scheme provided by the CompletionStage interface is very simple, which is simpler than try{}catch {}. The following are related methods. Using these methods for exception handling is the same as serial operation, and both support chain programming.

CompletionStage exceptionally(fn);
CompletionStage<R> whenComplete(consumer);
CompletionStage<R> whenCompleteAsync(consumer);
CompletionStage<R> handle(fn);
CompletionStage<R> handleAsync(fn);

example:

CompletableFuture<Integer> 
  f0 = CompletableFuture
    .supplyAsync(()->(7/0))
    .thenApply(r->r*10)
    .exceptionally(e->0);
System.out.println(f0.join());

case

Rewrite the example of boiling water and making tea pot with completable future (save pause time).

        CompletableFuture<Void> f1 =
                CompletableFuture.runAsync(()->{
                    System.out.println("T1:Kettle...");
                    System.out.println("T1:Boil water...");
                });

        CompletableFuture<String> f2 =
                CompletableFuture.supplyAsync(()->{
                    System.out.println("T2:Wash the teapot...");
                    System.out.println("T2:Wash the tea cup...");
                    System.out.println("T2:Take tea...");
                    return  "Longjing";
                });
        CompletableFuture f3 = f1.thenCombine(f2,(__,x)->{
            System.out.println("T1:Get the tea:"+x);
            System.out.println("T1:Make tea...");
            return "Serve tea:" + x;
        });
        System.out.println(f3.join());

Completable future advantages

  1. There is no need to maintain threads manually, there is no tedious work of manually maintaining threads, and the work of assigning threads to tasks does not need our attention;
  2. Clearer semantics, e.g. F3 = F1 Thencombine (F2, () - > {}) can clearly state that "task 3 cannot start until task 1 and task 2 are completed";
  3. The code is more concise and focuses on business logic. Almost all the code is related to business logic.

Topics: Java Multithreading