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
- thenCompose(): The second operation is performed when the first task is completed
- 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