Thinking logic of computer programs (94) and thinking 94

Source: Internet
Author: User
Tags stream api

Thinking logic of computer programs (94) and thinking 94

The previous two sections discussed functional data processing in Java 8, which is an enhancement to the container class introduced in sections 38 to 55, it can combine multiple operations on the set data in a pipeline. This section continues to discuss the new features of Java 8, mainly a new class CompletableFuture, which is an enhancement to concurrent programming introduced in sections 65 to 83, it can easily combine multiple dependencies of asynchronous tasks in a pipeline, greatly simplifying the development of multiple asynchronous tasks.

I have introduced so many concurrent programming contents before. What problems cannot be solved? What exactly can CompletableFuture solve? What is the relationship with the previous content? How to use it? What is the basic principle? This section discusses in detail. Let's take a look at the problems it will solve first.

Asynchronous Task Management

In modern software development, the system functions become more and more complex, and the management complexity is managed separately. Many functions of the system may be divided into small services to provide external Web APIs, independent development, deployment, and maintenance. For example, in an e-commerce system, there may be specialized product services, order services, user services, recommendation services, preferential services, and search services. When a page is displayed externally, multiple services may need to be called, and there may be some dependencies between multiple calls. For example, to display a product page, you need to call the product service, you may also need to call the recommendation service to obtain other recommendations related to the product. You may also need to call the preferential service to obtain the promotion discounts related to the product. In order to call the preferential service, you may need to call the user service to obtain the user's membership level first.

In addition, modern software often relies on many third-party services, such as map services, SMS services, weather services, and exchange rate services, you may need to access multiple such services, and there may be some dependencies between these accesses.

To improve performance and make full use of system resources, these calls to external services should generally be asynchronous and as concurrent as possible. We have introduced the asynchronous task execution service in section 77. ExecutorService can be used to conveniently submit a single independent asynchronous task and obtain the asynchronous task result through the Future interface as needed, however, this support is insufficient for multiple asynchronous tasks, especially those with certain dependencies.

Therefore, we have CompletableFuture, which is a specific class that implements two interfaces: Future and CompletionStage. Future indicates the result of an asynchronous task, completionStage literally means the completion stage. Multiple completionstages can be combined in pipelines. For one CompletionStage, it has a computing task, however, you may need to wait for one or more stages to complete before you can start. After it is completed, it may trigger other stages to start running. CompletionStage provides many methods to easily respond to task events, build a task pipeline, and implement combined asynchronous programming.

How to use it? Next we will gradually explain that CompletableFuture is also a Future. Let's first look at something similar to Future.

Comparison with Future/FutureTask

Basic task execution service

We will first briefly review the asynchronous task execution service and Future through examples. In the asynchronous task execution service, Callable or Runnable is used to represent the task. In the Callable example, a simulated external task is:

private static Random rnd = new Random();static int delayRandom(int min, int max) {    int milli = max > min ? rnd.nextInt(max - min) : 0;    try {        Thread.sleep(min + milli);    } catch (InterruptedException e) {    }    return milli;}static Callable<Integer> externalTask = () -> {    int time = delayRandom(20, 2000);    return time;};

ExternalTask indicates an external task. We use a Lambda expression. If you are not familiar with it, see section 91. delayRandom is used to simulate latency.

Assume that there is an asynchronous task execution service whose code is:

private static ExecutorService executor =        Executors.newFixedThreadPool(10);

Call an external service by executing a task. Generally, Future is returned, indicating the asynchronous result. The sample code is as follows:

public static Future<Integer> callExternalService(){    return executor.submit(externalTask);}

In the main program, the sample code combined with asynchronous tasks and local calls is:

Public static void master () {// execute the asynchronous task Future <Integer> asyncRet = callExternalService (); // execute other tasks... // obtain the result of an asynchronous task and handle possible exceptions. try {Integer ret = asyncRet. get (); System. out. println (ret);} catch (InterruptedException e) {e. printStackTrace ();} catch (ExecutionException e) {e. printStackTrace ();}}

Basic CompletableFuture

Similar functions can be implemented using CompletableFuture. However, it does not support Callable for asynchronous tasks, but Runnable and Supplier for asynchronous tasks with returned results, the difference with Callale is that it cannot throw a checked exception. If an exception occurs, it can throw a runtime exception.

Use Supplier to represent an asynchronous task. The code is similar to Callable. Replace the variable type, that is:

static Supplier<Integer> externalTask = () -> {    int time = delayRandom(20, 2000);    return time;};

The code for using CompletableFuture to call an external service can be:

public static Future<Integer> callExternalService(){    return CompletableFuture.supplyAsync(externalTask, executor);}

SupplyAsync is a static method defined:

public static <U> CompletableFuture<U> supplyAsync(    Supplier<U> supplier, Executor executor)

It accepts two parameters: supplier and executor. Internally, it uses executor to execute the task represented by supplier and returns a CompletableFuture. After the call, the task is executed asynchronously. This method returns immediately.

SupplyAsync has another method without the executor parameter:

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

Without executor, who will execute the task? It depends on the system environment and configuration. Generally, if the number of available CPU cores is greater than 2, the Fork/Join task execution service introduced by Java 7 is used, that is, ForkJoinPool. commonPool (), the number of worker threads behind the task execution service is generally reduced by 1 by the number of CPU cores, that is, Runtime. getRuntime (). availableProcessors ()-1. Otherwise, ThreadPerTaskExecutor is used to create a thread for each task.

For CPU-intensive computing tasks, it is appropriate to use Fork/Join tasks to execute services. However, Fork/Join may be inappropriate for general asynchronous tasks that call external services, because of its low degree of parallelism, it may allow concurrent multi-task serial running. In this case, the Executor parameter should be provided.

Later we will see many methods named after Async. Generally, there are two versions, one with the Executor parameter and the other with the same meaning, so we will not repeat them.

For tasks of the Runnable type, the method for building CompletableFuture is:

public static CompletableFuture<Void> runAsync(    Runnable runnable)public static CompletableFuture<Void> runAsync(    Runnable runnable, Executor executor)

It is similar to supplyAsync.

CompletableFuture: Basic enhancement to Future

The Future interfaces and CompletableFuture are supported. However, CompletableFuture also has some additional related methods, such:

public T join()public boolean isCompletedExceptionally()public T getNow(T valueIfAbsent)

The join method is similar to the get method. It also waits for the task to end, but it does not throw a checked exception. If the task exception ends, join will encapsulate the exception as a runtime exception CompletionException and throw it.

Future has the isDone method to check whether the task is finished, but does not know whether the task ends normally or abnormally. The isCompletedExceptionally method can determine whether the task is ended abnormally.

GetNow is similar to join. The difference is that if the task is not finished, it will not wait, but will return the passed parameter valueIfAbsent.

Further understanding of Future/CompletableFuture

The previous example uses the task execution service. In fact, the task execution service and the asynchronous result Future are not tied together. You can create a thread to return asynchronous results. For further understanding, let's look at some examples.

Use FutureTask to call external services. The code can be:

public static Future<Integer> callExternalService() {    FutureTask<Integer> future = new FutureTask<>(externalTask);    new Thread() {        public void run() {            future.run();        }    }.start();    return future;}

A thread is created internally and the thread calls the run method of FutureTask. We analyzed the code of FutureTask in section 77. The run method calls the call method of externalTask, and save the results or encountered exceptions to wake up the thread waiting for the results.

With CompletableFuture, you can also directly create a thread and return asynchronous results. The code can be:

public static Future<Integer> callExternalService() {    CompletableFuture<Integer> future = new CompletableFuture<>();    new Thread() {        public void run() {            try {                future.complete(externalTask.get());            } catch (Exception e) {                future.completeExceptionally(e);            }        }    }.start();    return future;}

Here we use two methods of CompletableFuture:

public boolean complete(T value)public boolean completeExceptionally(Throwable ex) 

The two methods explicitly set the task status and result. The complete setting task is successfully completed and the result is value. completeExceptionally, the task ends abnormally and the exception is ex. There is no corresponding method for the Future interface. The FutureTask has related methods but is not public (it is protected ). After they are set, they will trigger other completionstages dependent on them. What will it trigger? Let's take a look.

Response Results or exceptions

With Future, we can only get results through get, while get may need to block the wait. With CompletionStage, we can register a callback function and automatically trigger execution when the task is completed or an exception ends, there are two types of registration methods: whenComplete and handle. Let's take a look at them.

WhenComplete

WhenComplete declaration:

public CompletableFuture<T> whenComplete(    BiConsumer<? super T, ? super Throwable> action)

The parameter action indicates the callback function. It is called no matter whether the previous stage ends normally or abnormally. The function type is BiConsumer. Two parameters are accepted, the first parameter is the result value at the normal end, the second parameter is the exception at the end of the exception, and BiConsumer does not return a value. The return value of whenComplete is still CompletableFuture. It does not change the results of the original stage, and can continue to call other functions on it. Let's look at a simple example:

CompletableFuture.supplyAsync(externalTask).whenComplete((result, ex) -> {    if (result != null) {        System.out.println(result);    }    if (ex != null) {        ex.printStackTrace();    }}).join();

Result indicates the result of the previous stage, and ex indicates an exception. Only one result may not be null.

Who executes the whenComplete registration function? Generally, this depends on the status of the task at registration. If the task is not completed at registration, the registered function will be executed by the thread that executes the task, after this thread executes the task, it executes the registered function. if the task is completed at the time of registration, it is executed by the current thread (that is, the thread that calls the registered function.

If you do not want the current thread to be executed to avoid possible synchronization congestion, you can use the other two asynchronous registration methods:

public CompletableFuture<T> whenCompleteAsync(    BiConsumer<? super T, ? super Throwable> action)public CompletableFuture<T> whenCompleteAsync(    BiConsumer<? super T, ? super Throwable> action, Executor executor)       

As with the method ending with Async described earlier, for the first method, the registration function action will be executed by the default Task Service (that is, ForkJoinPool. commonPool () or ThreadPerTaskExecutor). The second method is executed by the executor specified in the parameter.

Handle

WhenComplete only registers the callback function and does not change the result. It returns a CompletableFuture, but the result of CompletableFuture is the same as that of CompletableFuture. There is also a similar registration method handle, the statement is as follows:

public <U> CompletableFuture<U> handle(    BiFunction<? super T, Throwable, ? extends U> fn)

The callback function is a BiFunction and accepts two parameters. One is a normal result and the other is an exception, but BiFunction has a return value. In the CompletableFuture returned by handle, the result is replaced by the BiFunction return value, even if an exception exists, it will be overwritten, for example:

String ret =    CompletableFuture.supplyAsync(()->{        throw new RuntimeException("test");    }).handle((result, ex)->{        return "hello";    }).join();System.out.println(ret);

The output is "hello ". The asynchronous task throws an exception, but the result is changed through the handle method.

Like whenComplete, handle also has the corresponding asynchronous registration method handleAsync, which we will not discuss.

Predictionally

WhenComplete and handle both respond to normal completion and respond to exceptions. If you are only interested in exceptions, you can use exceptionally. Its declaration is:

public CompletableFuture<T> exceptionally(    Function<Throwable, ? extends T> fn)

The callback Function registered by it is a Function. The accepted parameter is an exception and a value is returned. Similar to handle, it also changes the result. This is not an example.

In addition to the response results and exceptions, you can use CompletableFuture to easily construct a task stream with multiple dependencies. Let's look at the simple dependency of a single stage.

Build a task flow that depends on a single stage

ThenRun

After a stage is completed normally, execute the next task and take a simple example:

Runnable taskA = () -> System.out.println("task A");Runnable taskB = () -> System.out.println("task B");Runnable taskC = () -> System.out.println("task C");CompletableFuture.runAsync(taskA)    .thenRun(taskB)    .thenRun(taskC)    .join();

Here, there are three asynchronous tasks taskA, taskB, and taskC, which naturally describe their dependencies through thenRun. thenRun is a synchronous version with the corresponding asynchronous version thenRunAsync:

public CompletableFuture<Void> thenRunAsync(Runnable action)public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)

In the task flow built by thenRun, the task in the next stage is executed only when the previous stage does not end abnormally. If an exception occurs in the previous stage, all subsequent stages will not run, the results will be set to the same exception. Calling join will throw a runtime exception CompletionException.

The next task type specified by thenRun is Runnable. It does not need the results of the previous stage as parameters or return values. Therefore, in CompletableFuture returned by thenRun, The result type is Void, no results.

ThenAccept/thenApply

If the result of the previous stage is required as a parameter for the next task, you can use the thenAccept or thenApply method:

public CompletableFuture<Void> thenAccept(    Consumer<? super T> action)public <U> CompletableFuture<U> thenApply(    Function<? super T,? extends U> fn)

The Task Type of thenAccept is Consumer. It accepts the result of the previous stage as a parameter and has no return value. The Task Type of thenApply is Function. If the result of the previous stage is accepted as a parameter, a new value is returned, which will be the result value of CompletableFuture returned by thenApply. Let's look at a simple example:

Supplier<String> taskA = () -> "hello";Function<String, String> taskB = (t) -> t.toUpperCase();Consumer<String> taskC = (t) -> System.out.println("consume: " + t);CompletableFuture.supplyAsync(taskA)    .thenApply(taskB)    .thenAccept(taskC)    .join();

The result of taskA is "hello", passed to taskB, taskB is converted to "HELLO", and then the result is output to taskC, so the output is:

consume: HELLO

CompletableFuture has many methods with names including run, accept, and apply. They generally correspond to the task type, run corresponds to Runnable, accept corresponds to Consumer, and apply corresponds to Function, I will not go into details later.

ThenCompose

Similar to thenApply, there is also a method thenCompose, declared:

public <U> CompletableFuture<U> thenCompose(    Function<? super T, ? extends CompletionStage<U>> fn)

This task type is also a Function. It also accepts the results of the previous stage and returns a new result. However, the type of return value of this conversion Function fn is CompletionStage, that is, the returned value is also a stage. If thenApply is used, the result will be changed to CompletableFuture <U>. If thenCompose is used, the CompletionStage returned by fn will be directly returned, and the difference between thenCompose and thenApply will be returned, just like the difference between flatMap and map in Stream API, let's look at a simple example:

Supplier<String> taskA = () -> "hello";Function<String, CompletableFuture<String>> taskB = (t) ->    CompletableFuture.supplyAsync(() -> t.toUpperCase());Consumer<String> taskC = (t) -> System.out.println("consume: " + t);CompletableFuture.supplyAsync(taskA)    .thenCompose(taskB)    .thenAccept(taskC)    .join();

In the above Code, taskB is a conversion function, but it also executes an asynchronous task and the return type is CompletableFuture. Therefore, thenCompose is used.

Build a task flow that depends on two phases

Both dependencies are complete.

ThenRun, thenAccept, thenApply, and thenCompose are used to execute another task after a stage is completed. CompletableFuture also has some methods to execute another task after both stages are completed:

public CompletableFuture<Void> runAfterBoth(    CompletionStage<?> other, Runnable actionpublic <U,V> CompletableFuture<V> thenCombine(    CompletionStage<? extends U> other,    BiFunction<? super T,? super U,? extends V> fn)public <U> CompletableFuture<Void> thenAcceptBoth(    CompletionStage<? extends U> other,    BiConsumer<? super T, ? super U> action) 

RunAfterBoth corresponds to Runnable, and thenCombine corresponds to BiFunction. If the results of the first two stages are accepted as parameters, a result is returned. The task type corresponding to thenAcceptBoth is BiConsumer, the results of the first two stages are accepted as parameters, but no results are returned. They all have the corresponding asynchronous and Executor parameters, which are used to specify who will execute the next task. There is no dependency between the current phase and the other stage specified by the parameter. Concurrent execution starts. When both stages are executed, the specified task starts to be executed.

Let's look at A simple example. After task A and Task B are executed, execute task C to merge the results. The code is:

Supplier<String> taskA = () -> "taskA";CompletableFuture<String> taskB = CompletableFuture.supplyAsync(() -> "taskB");BiFunction<String, String, String> taskC = (a, b) -> a + "," + b;String ret = CompletableFuture.supplyAsync(taskA)        .thenCombineAsync(taskB, taskC)        .join();System.out.println(ret);

Output:

taskA,taskB

One of the two stages of dependency

The preceding method requires that the next task be executed only after both stages are completed. If you only need to complete any stage, you can use the following method:

public CompletableFuture<Void> runAfterEither(    CompletionStage<?> other, Runnable action)public <U> CompletableFuture<U> applyToEither(    CompletionStage<? extends T> other, Function<? super T, U> fn)public CompletableFuture<Void> acceptEither(    CompletionStage<? extends T> other, Consumer<? super T> action)          

They all have the corresponding asynchronous and Executor parameters, which are used to specify who will execute the next task. There is no dependency between the current stage and the other of another stage specified by the parameter. Concurrent execution is performed. Once one of the stages is completed, another task specified by the parameter is started, which is not described in detail.

Build a task flow that depends on multiple stages

If there are more than two dependent stages, you can use the following method:

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

They are static methods, and a new CompletableFuture is built based on multiple CompletableFuture.

For allOf, it completes only when all child CompletableFuture is completed. If some CompletableFuture exceptions end, the results of the new CompletableFuture are also abnormal. However, it does not end early because of exceptions, but waits until all stages end. If multiple stages end abnormally, the exception saved in the new CompletableFuture is the last one. The new CompletableFuture will hold the exception results, but will not save the normal ending results. If necessary, you can obtain the results from each stage. Let's look at a simple example:

CompletableFuture<String> taskA = CompletableFuture.supplyAsync(() -> {    delayRandom(100, 1000);    return "helloA";}, executor);CompletableFuture<Void> taskB = CompletableFuture.runAsync(() -> {    delayRandom(2000, 3000);}, executor);CompletableFuture<Void> taskC = CompletableFuture.runAsync(() -> {    delayRandom(30, 100);    throw new RuntimeException("task C exception");}, executor);CompletableFuture.allOf(taskA, taskB, taskC).whenComplete((result, ex) -> {    if (ex != null) {        System.out.println(ex.getMessage());    }    if (!taskA.isCompletedExceptionally()) {        System.out.println("task A " + taskA.join());    }});

TaskC will end abnormally first, but the newly constructed CompletableFuture will wait for the other two to end. After both ends, you can check the status and results of the sub-phase through the sub-phase (such as taskA) method.

For CompletableFuture returned by anyOf, when the first child CompletableFuture completes or the exception ends, it completes or the exception ends accordingly. The result is the same as that of the first child CompletableFuture.

Summary

This section describes the combined asynchronous programming CompletableFuture in Java 8:

  • It is an enhancement to the Future, but it can respond to results or exceptions. There are many ways to build asynchronous task streams
  • There are generally three types of corresponding methods based on who executes the task. Methods with names without Async are executed by the current thread or the thread of the previous stage, the method with Async but no Executor is specified is executed by the default Excecutor (ForkJoinPool. commonPool () or ThreadPerTaskExecutor). Methods with Async and specified Executor parameters are executed by the specified Executor.
  • According to the task type, there are generally three types of corresponding methods. The names include Runnable with run, Consumer with accept, and Function with apply.

With CompletableFuture, you can easily describe the dependencies and execution processes between multiple asynchronous tasks, greatly simplifying the code and improving readability.

In the next section, we will discuss Java 8's enhancements to date and time APIs.

(As in other chapters, all the code in this section is located at https://github.com/swiftma/program-logicand under Bao shuo.laoma.java8.c94)

----------------

For more information, see the latest article. Please pay attention to the Public Account "lauma says programming" (scan the QR code below), from entry to advanced, ma and you explore the essence of Java programming and computer technology. Retain All copyrights with original intent.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.