When we execute a task asynchronously, we usually use the thread pool Executor to create it. If no return value is required, the task implements the Runnable interface; If a return value is required, the task implements the Callable interface, calls the submit method of the Executor, and then uses Future to obtain it. What should we do if multiple threads have dependency combinations? The synchronization components CountDownLatch and CyclicBarrier can be used, but they are troublesome. In fact, there is a simple method, which is to use CompeletableFuture. Recently, I just optimized the code in the project with completable Future, so I'll learn completable Future with you.
- Official account: little boy picking up snail
- github address
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 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"); } } Copy code
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"); } } Copy code
Operation results:
Total time 806 ms Copy code
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"); } } Copy code
It can be found that the code is much simpler using completable future. The supplyAsync 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
Create asynchronous task
Completable future creates asynchronous tasks. Generally, there are two methods: supplyAsync and runAsync
- 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) Copy code
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) Copy code
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:Little boy picking snails"), executor); //Use of supplyAsync CompletableFuture<String> supplyFuture = CompletableFuture.supplyAsync(() -> { System.out.print("supply,Official account:Little boy picking snails"); return "Little boy picking 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,Official account:Little boy picking snails null supply,Official account:Little boy picking snails little boy picking snails Copy code
Task asynchronous callback
1. thenRun/thenRunAsync
public CompletableFuture<Void> thenRun(Runnable action); public CompletableFuture<Void> thenRunAsync(Runnable action); Copy code
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 one first CompletableFuture Method task"); return "Little boy picking snails"; } ); CompletableFuture thenRunFuture = orgFuture.thenRun(() -> { System.out.println("Then perform the second task"); }); System.out.println(thenRunFuture.get()); } } //output Execute the first one first CompletableFuture Method task Then perform the second task null Copy code
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); } Copy code
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 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, but the callback method does not return a value.
public class FutureThenAcceptTest { public static void main(String[] args) throws ExecutionException, InterruptedException { CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync( ()->{ System.out.println("original CompletableFuture Method task"); return "Little boy picking snails"; } ); CompletableFuture thenAcceptFuture = orgFuture.thenAccept((a) -> { if ("Little boy picking snails".equals(a)) { System.out.println("Attention"); } System.out.println("Think about it first"); }); System.out.println(thenAcceptFuture.get()); } } Copy code
3. 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 CompletableFuture Method task"); return "Little boy picking snails"; } ); CompletableFuture<String> thenApplyFuture = orgFuture.thenApply((a) -> { if ("Little boy picking snails".equals(a)) { return "Attention"; } return "Think about it first"; }); System.out.println(thenApplyFuture.get()); } } //output original CompletableFuture Method task Attention Copy code
4. 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 Copy code
5. 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 "Little boy picking snails"; } ); CompletableFuture<String> rstFuture = orgFuture.whenComplete((a, throwable) -> { System.out.println("Current thread Name:" + Thread.currentThread().getName()); System.out.println("The last task has been completed, and the" + a + "Pass it on"); 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 Little boy picking snails Copy code
6. 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(); } return "Little boy picking snails"; } ); CompletableFuture<String> rstFuture = orgFuture.handle((a, throwable) -> { System.out.println("The last task has been completed, and the" + a + "Pass it on"); if ("Little boy picking snails".equals(a)) { System.out.println("666"); return "Attention"; } System.out.println("233333"); return null; }); System.out.println(rstFuture.get()); } } //output Current thread Name: ForkJoinPool.commonPool-worker-1 When the last task was finished, I passed the little boy who picked up snails 666 Attention Copy code
Multiple task combination processing
AND combination relationship
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 { CompletableFuture<String> first = CompletableFuture.completedFuture("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 "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 Copy code
OR combination relationship
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("Complete the first asynchronous task");} catch (Exception e){ return "First task exception"; } return "First asynchronous task"; }); ExecutorService executor = Executors.newSingleThreadExecutor(); CompletableFuture<Void> future = CompletableFuture //Second asynchronous task .supplyAsync(() -> { System.out.println("Finish the second task"); return "Second task";} , executor) //Third task .acceptEitherAsync(first, System.out::println, executor); executor.shutdown(); } } //output Finish the second task Second task Copy code
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'm done"); }); CompletableFuture<Void> b = CompletableFuture.runAsync(() -> { System.out.println("I'm done, too"); }); CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(a, b).whenComplete((m,k)->{ System.out.println("finish"); }); } } //output I'm done I'm done, too finish Copy code
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'm done"); }); CompletableFuture<Void> b = CompletableFuture.runAsync(() -> { System.out.println("I'm done, 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 Copy code
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 { CompletableFuture<String> f = CompletableFuture.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 Copy code
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.
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(); Copy code
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); Copy code
3. Precautions 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.
4. 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.
Author: a little boy picking up snails
Link: https://juejin.cn/post/6970558076642394142
Source: rare earth Nuggets
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.