Java 8 - Asynchronous programming (with source code)

Posted by konetch on Fri, 09 Aug 2019 04:32:53 +0200

Asynchronous programming

Asynchronism is actually a way to keep an operation running without waiting for the return value of the called function.

(If you want to learn programming by yourself, please search Circle T Community More industry-related information and more industry-related free video tutorials. It's totally free!

Create and execute tasks

No Reference Creation
 CompletableFuture<String> noArgsFuture = new CompletableFuture<>();
Pass in the corresponding task, no return value

The runAsync method can perform asynchronous computation in the background, but there is no return value at this time. Hold a Runnable object.

CompletableFuture noReturn = CompletableFuture.runAsync(()->{
    //Execution logic, no return value
});
Pass in the corresponding task with a return value

At this point we see that the return is Completable Future < T > where T is the type of return value you want. Supplier < T > is a simple functional interface.

CompletableFuture<String> hasReturn = CompletableFuture.supplyAsync(new Supplier<String>() {
    @Override
    public String get() {
        return "hasReturn";
    }
});

At this point, lambda expressions can be used to make the above logic clearer

CompletableFuture<String> hasReturnLambda = CompletableFuture.supplyAsync(TestFuture::get);

private static String get() {
    return "hasReturnLambda";
}

Get the return value

Asynchronous tasks also have return values. When we want to use the return values of asynchronous tasks, we can call get() of CompletableFuture to block until there are return values for asynchronous tasks.

We modified the get() method above to pause for ten seconds.

private static String get() {
    System.out.println("Begin Invoke getFuntureHasReturnLambda");
    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {

    }
    System.out.println("End Invoke getFuntureHasReturnLambda");
    return "hasReturnLambda";
}

Then make a call.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<String> funtureHasReturnLambda = (CompletableFuture<String>) getFuntureHasReturnLambda();
    System.out.println("Main Method Is Invoking");
    funtureHasReturnLambda.get();
    System.out.println("Main Method End");
}

As you can see, the output is as follows. The current thread is blocked only when the get() method is called.

Main Method Is Invoking
Begin Invoke getFuntureHasReturnLambda
End Invoke getFuntureHasReturnLambda
Main Method End

Custom return value

In addition to waiting for the return value of an asynchronous task, we can also call the complete() method at any time to customize the return value.

CompletableFuture<String> funtureHasReturnLambda = (CompletableFuture<String>) getFuntureHasReturnLambda();
System.out.println("Main Method Is Invoking");
new Thread(()->{
    System.out.println("Thread Is Invoking ");
    try {
        Thread.sleep(1000);
        funtureHasReturnLambda.complete("custome value");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("Thread End ");
}).run();
String value = funtureHasReturnLambda.get();
System.out.println("Main Method End value is "+ value);

We can find that the output is the output value of the new thread. Of course, this is because our asynchronous method sets the waiting time of 10 seconds. If the asynchronous method waits for 1 seconds and the new thread waits for 10 seconds, then the output value is the value of the asynchronous method.

Main Method Is Invoking
Begin Invoke getFuntureHasReturnLambda
Thread Is Invoking 
Thread End 
Main Method End value is custome value

Perform asynchronous tasks sequentially

If the completion of an asynchronous task depends on the completion of the previous asynchronous task, how do you write it? Is it to call get() method to get the return value and then execute it? This is a bit cumbersome to write, and Completable Future provides us with a way to fulfill the requirements that we want to perform some asynchronous tasks sequentially. The nApply, the nAccept and the nRun are three methods. The difference between these three methods is.

Method name Is the return value of the previous task available? Is there a return value?
thenApply Achieving Yes
thenAccept Achieving nothing
thenRun Not available nothing

So in general, thenAccept and thenRun are used at the end of the call chain. Next, let's take a real example.

//thenApply retrieves the return value of the previous task, as well as the return value.
CompletableFuture<String> seqFutureOne = CompletableFuture.supplyAsync(()-> "seqFutureOne");
CompletableFuture<String> seqFutureTwo = seqFutureOne.thenApply(name -> name + " seqFutureTwo");
System.out.println(seqFutureTwo.get());


//thenAccept retrieves the return value of the previous task, but has no return value
CompletableFuture<Void> thenAccept = seqFutureOne
        .thenAccept(name -> System.out.println(name + "thenAccept"));
System.out.println("-------------");
System.out.println(thenAccept.get());

//thenRun does not get the return value of the previous task, nor does it have a return value.
System.out.println("-------------");
CompletableFuture<Void> thenRun = seqFutureOne.thenRun(() -> {
    System.out.println("thenRun");
});
System.out.println(thenRun.get());

The information returned is as follows

seqFutureOne seqFutureTwo
seqFutureOnethenAccept
-------------
null
-------------
thenRun
null

The difference between the nApply and the nApply Async

We can find that all three methods have a method suffixed with Async, such as the nApply Async. So what's the difference between the Async approach and the non-suffix approach? We compare the two methods of thenApply and thenApply Async, and the others are the same.
The difference between the two methods is who will perform the task. If the nApply Async is used, the thread of execution is from
ForkJoinPool.commonPool() gets different threads to execute. If using thenApply, if the supplyAsync method executes very fast, then thenApply task is executed by the main thread, and if executed very slowly, it is the same as the supplyAsync thread. Next, let's take an example to show how fast supplyAsync can be executed using sleep method.

//The difference between the nApply and the nApply Async
System.out.println("-------------");
CompletableFuture<String> supplyAsyncWithSleep = CompletableFuture.supplyAsync(()->{
    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "supplyAsyncWithSleep Thread Id : " + Thread.currentThread();
});
CompletableFuture<String> thenApply = supplyAsyncWithSleep
        .thenApply(name -> name + "------thenApply Thread Id : " + Thread.currentThread());
CompletableFuture<String> thenApplyAsync = supplyAsyncWithSleep
        .thenApplyAsync(name -> name + "------thenApplyAsync Thread Id : " + Thread.currentThread());
System.out.println("Main Thread Id: "+ Thread.currentThread());
System.out.println(thenApply.get());
System.out.println(thenApplyAsync.get());
System.out.println("-------------No Sleep");
CompletableFuture<String> supplyAsyncNoSleep = CompletableFuture.supplyAsync(()->{
    return "supplyAsyncNoSleep Thread Id : " + Thread.currentThread();
});
CompletableFuture<String> thenApplyNoSleep = supplyAsyncNoSleep
        .thenApply(name -> name + "------thenApply Thread Id : " + Thread.currentThread());
CompletableFuture<String> thenApplyAsyncNoSleep = supplyAsyncNoSleep
        .thenApplyAsync(name -> name + "------thenApplyAsync Thread Id : " + Thread.currentThread());
System.out.println("Main Thread Id: "+ Thread.currentThread());
System.out.println(thenApplyNoSleep.get());
System.out.println(thenApplyAsyncNoSleep.get());

We can see that the output is

-------------
Main Thread Id: Thread[main,5,main]
supplyAsyncWithSleep Thread Id : Thread[ForkJoinPool.commonPool-worker-1,5,main]------thenApply Thread Id : Thread[ForkJoinPool.commonPool-worker-1,5,main]
supplyAsyncWithSleep Thread Id : Thread[ForkJoinPool.commonPool-worker-1,5,main]------thenApplyAsync Thread Id : Thread[ForkJoinPool.commonPool-worker-1,5,main]
-------------No Sleep
Main Thread Id: Thread[main,5,main]
supplyAsyncNoSleep Thread Id : Thread[ForkJoinPool.commonPool-worker-2,5,main]------thenApply Thread Id : Thread[main,5,main]
supplyAsyncNoSleep Thread Id : Thread[ForkJoinPool.commonPool-worker-2,5,main]------thenApplyAsync Thread Id : Thread[ForkJoinPool.commonPool-worker-2,5,main]

You can see that if the supplyAsync method executes slowly, the thread of thenApply method executes is the same as that of supplyAsync method. If the supplyAsync method executes quickly, the thread of thenApply method executes is the same as that of Main method.

Combination Completable Future

There are two ways to combine two Completable Futures

  1. thenCompose(): The second operation is performed when the first task is completed
  2. thenCombine(): Certain operations are performed only when both asynchronous tasks are completed

The nCompose () Usage

We define two asynchronous tasks, assuming that the second timing task needs the return value of the first timing task.

public static CompletableFuture<String> getTastOne(){
    return CompletableFuture.supplyAsync(()-> "topOne");
}

public static CompletableFuture<String> getTastTwo(String s){
    return CompletableFuture.supplyAsync(()-> s + "  topTwo");
}

We use the thenCompose() method to write

CompletableFuture<String> thenComposeComplet = getTastOne().thenCompose(s -> getTastTwo(s));
System.out.println(thenComposeComplet.get());

The output is

topOne  topTwo

If you remember the previous thenApply() method, you should think that this method can achieve similar functions with thenApply().

//thenApply
CompletableFuture<CompletableFuture<String>> thenApply = getTastOne()
        .thenApply(s -> getTastTwo(s));
System.out.println(thenApply.get().get());

But we found that the return value is a nested return type, and to get the final return value, we need to call get() twice.

The nCombine () Usage

For example, we need to calculate the sum of the return values of two asynchronous methods. The summation operation can only be calculated if two asynchronous methods are used to get the value, so we can use the thenCombine() method to calculate.

CompletableFuture<Integer> thenComposeOne = CompletableFuture.supplyAsync(() -> 192);
CompletableFuture<Integer> thenComposeTwo = CompletableFuture.supplyAsync(() -> 196);
CompletableFuture<Integer> thenComposeCount = thenComposeOne
        .thenCombine(thenComposeTwo, (s, y) -> s + y);
System.out.println(thenComposeCount.get());

When both thenComposeOne and thenComposeTwo are completed, the callback function passed to the thenCombine method is invoked.

Combining multiple Completable Future

On the above, we assemble two Completable Futures using the thenCompose() and thenCombine() methods. If we want to combine any number of Completable Futures, we can use the following two methods.

  • allOf(): Waiting for all Completable Futures to complete before running the callback function
  • anyOf(): As long as one of the Completable Futures is complete, the callback function is executed. Note that other tasks will not be performed at this time.

Next, I'll show you how to use the two methods.

//allOf()
CompletableFuture<Integer> one = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> two = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> three = CompletableFuture.supplyAsync(() -> 3);
CompletableFuture<Integer> four = CompletableFuture.supplyAsync(() -> 4);
CompletableFuture<Integer> five = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<Integer> six = CompletableFuture.supplyAsync(() -> 6);

CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(one, two, three, four, five, six);
voidCompletableFuture.thenApply(v->{
    return Stream.of(one,two,three,four, five, six)
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
}).thenAccept(System.out::println);

CompletableFuture<Void> voidCompletableFuture1 = CompletableFuture.runAsync(() -> {
    try {
        Thread.sleep(1000);
    } catch (Exception e) {

    }
    System.out.println("1");
});

We define six Completable Futures to wait for all Completable Futures to complete all tasks and then output their values.

The use of anyOf()

CompletableFuture<Void> voidCompletableFuture1 = CompletableFuture.runAsync(() -> {
try {
    Thread.sleep(1000);
} catch (Exception e) {

}
System.out.println("voidCompletableFuture1");
});

CompletableFuture<Void> voidCompletableFutur2 = CompletableFuture.runAsync(() -> {
try {
    Thread.sleep(2000);
} catch (Exception e) {

}
System.out.println("voidCompletableFutur2");
});

CompletableFuture<Void> voidCompletableFuture3 = CompletableFuture.runAsync(() -> {
try {
    Thread.sleep(3000);
} catch (Exception e) {

}
System.out.println("voidCompletableFuture3");
});

CompletableFuture<Object> objectCompletableFuture = CompletableFuture
    .anyOf(voidCompletableFuture1, voidCompletableFutur2, voidCompletableFuture3);
objectCompletableFuture.get();

Here we define three Completable Futures for some time-consuming tasks, at which point the first Completable Futures will take the lead. The printing results are as follows.

voidCompletableFuture1

exception handling

We learned how Completable Future is executed asynchronously, how to combine different Completable Future, and how to execute Completable Future sequentially. The next important step, then, is what to do if an exception occurs while performing an asynchronous task. Let's start with an example.

CompletableFuture.supplyAsync(()->{
    //exception occurred
    int i = 10/0;
    return "Success";
}).thenRun(()-> System.out.println("thenRun"))
.thenAccept(v -> System.out.println("thenAccept"));

CompletableFuture.runAsync(()-> System.out.println("CompletableFuture.runAsync"));

As a result, we find that as long as one of the execution chains has an exception, the next chain will not execute, but the other Completable Futures under the mainstream process will still run.

CompletableFuture.runAsync
exceptionally()

We can use exceptionally to handle exceptions

//Handling exceptions

CompletableFuture<String> exceptionally = CompletableFuture.supplyAsync(() -> {
    //exception occurred
    int i = 10 / 0;
    return "Success";
}).exceptionally(e -> {
    System.out.println(e);
    return "Exception has Handl";
});
System.out.println(exceptionally.get());

Print as follows, you can find that the received value is abnormal information, but also can return a custom return value.

java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
Exception has Handl
handle()

Calling handle() also captures exceptions and customizes the return value. Unlike the exceptionally() method, handle() method is called regardless of whether an exception occurs. Examples are as follows

System.out.println("-------Abnormal-------");
CompletableFuture.supplyAsync(()->{
    //exception occurred
    int i = 10/0;
    return "Success";
}).handle((response,e)->{
    System.out.println("Exception:" + e);
    System.out.println("Response:" + response);
    return response;
});

System.out.println("-------No abnormality-------");
CompletableFuture.supplyAsync(()->{
    return "Sucess";
}).handle((response,e)->{
    System.out.println("Exception:" + e);
    System.out.println("Response:" + response);
    return response;
});

Printed as follows, we can see that the handle() method is also called when no exception occurs.

-------Abnormal-------
Exception:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
Response:null
-------No abnormality-------
Exception:null
Response:Sucess

Source code address

Topics: Java Programming Lambda