[Java Concurrent Programming] Best Practices for Using CompletableFuture


CompletableFuture is an enhanced version of Future and a powerful tool for multi-threaded development. This article introduces the usage of CompletableFuture in an easy-to-understand manner, and finally presents the general usage paradigm of CompletableFuture, which is ready to use out of the box and has reliable quality.

1. What is CompletableFuture?

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    
    
    
}
  • Future: represents the result of asynchronous calculation
  • CompletionStage: represents a stage of asynchronous calculation and represents complex calculations

As can be seen from the above definition, CompletableFuture not only implements the Future interface, but also implements the CompletionStage interface, which is an enhancement to the Future function. Introduced since Java 8 CompletableFuture, it Futurehas been improved and can pass in a callback object. When the asynchronous task is completed or an exception occurs, the callback method of the callback object is automatically called.

2. Why CompletableFuture is needed

When using Futureto obtain asynchronous execution results, you must either call a blocking method get()or poll to see isDone()if it is true. Neither of these methods is very good because the main thread will also be forced to wait.

CompletableFuture can also come into play when asynchronous callbacks and support for complex calculations are needed.

3. Use CompletableFuture

There are many methods in the CompletableFuture class. Memorizing them simply is troublesome. We need to classify them.

Create class

  • supplyAsync: asynchronous execution, return value

  • runAsync: asynchronous execution, no return value

  • anyOf: After any execution is completed, you can proceed to the next step.

  • allOf: Complete all tasks before proceeding to the next task

// 返回一个0
CompletableFuture.supplyAsync(() -> 0);

Continuation class (thenXxx)

  • The continuation class is the most important feature of CompletableFuture. Without this, CompletableFuture would be meaningless and is used to inject callback behavior.

  • We know that Java 8 functional interfaces have four common types: Function, Supplier, Consumer, and Runnable. What a coincidence is that CompletableFuture’s resume method also supports these four types of interfaces. The one-to-one correspondence is listed in the following table:

Java 8 Functional Interface Continuation method of CompletableFuture illustrate
Function (has parameters and returns) thenApply method Not using thenApplyAsync async
Supplier (no parameters, return) supplyAsync method, runAsync method Used when creating CompletableFuture
Consumer (with parameters and no return) thenAccept method Not using thenAcceptAsync async
Runnable (no parameter no return) thenRun method Not using thenRunAsync async
  • Just use the thenXxx method. You must use the asynchronous method with the Async suffix. Because the latter step depends on the results of the previous step
// 以异步计算1+2+3为例
CompletableFuture.supplyAsync(() -> {
    
    
    return 0;
}).thenApply(v -> {
    
    
    try {
    
     TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
    
     e.printStackTrace(); }
    return v + 1;
}).thenApply(v -> {
    
    
    try {
    
     TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
    
     e.printStackTrace(); }
    return v + 2;
}).thenApply(v -> {
    
    
    try {
    
     TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
    
     e.printStackTrace(); }
    return v + 3;
});
System.out.println("main线程可以异步干点别的事情,不用等待计算完成");

4. The general paradigm of using CompletableFuture

Generally, we use CompletableFuture to use multi-threading asynchronously to speed up queries. There are more read operations than write operations, so it is necessary to create multiple CompletableFuture objects.

Assume that in the microservice architecture, it is necessary to return the user's order and delivery address at once, and this information is in the order microservice and delivery address microservice respectively. In order to quickly respond to the user, it is necessary to choose to open multiple threads to query at the same time. The two microservices finally combine the query results and return them to the user.

Generally, you can use the following general paradigm of CompletableFuture, copy and paste it out of the box.

Result result = new Result();
// <1> 查询订单
CompletableFuture<List<Order>> orderCompletableFuture = CompletableFuture.supplyAsync(() -> {
    
    
    // <1.1> http查询订单
    List<Order> orderList = queryOrderList(uid);
    // <1.2> 设置结果集
    result.setOrderList(orderList);
    return orderList;
})
// <2> 后续处理
.thenApply(v -> {
    
    
    System.out.println("此处可以继续处理...");
    return v;
});

CompletableFuture<List<Address>> addressCompletableFuture = CompletableFuture.supplyAsync(() -> {
    
    
    List<Address> addressList = queryAddressList(uid);
    result.setAddressList(addressList);
    return addressList;
});

// <3> 等待所有任务执行完成
CompletableFuture.allOf(orderCompletableFuture, addressCompletableFuture);

// <4> 记录异常信息并抛出
List<String> errMessage = new ArrayList<>();
List<CompletableFuture<?>> completableFutures = Arrays.asList(whenComplete, whenComplete1);
for (int i = 0; i < completableFutures.size(); i++) {
    
    
    CompletableFuture<?> future = completableFutures.get(i);
    int finalI = i;
    future.exceptionally(ex -> {
    
    
        String str = "列表位置索引" + finalI + "处发生异常,异常信息是" + ex.getMessage();
        errMessage.add(str);
        throw new RuntimeException("Error occurred: " + ex);
    });
}

// <5> 异常处理
if(CollUtil.isNotEmpty(errMessage)){
    
    
    log.error("计算过程发生异常,异常有{}处,异常详细信息是:{}", errMessage.size(), errMessage);
    throw new RuntimeException("存在异常,可能有多处请逐个排查,异常信息列表是" + errMessage);
}
// <6> 返回结果给用户
return result;
  • Here <1>, a method is used CompletableFuture.supplyAsync, which is asynchronous and returns. This is the most commonly used method.

    The supplyAsync method is asynchronous and has a return value, while runAsync is asynchronous and has no return value. The supplyAsync method is more versatile. Just select it and you're done.

    • Here <1.1>, the order can be queried and parsed from the order microservice through RestTemplate or Ribbon, etc.
    • At <1.2>, set the order information into the result set
  • Here <2>, the information from the previous step can be further processed. Optional.

    Methods such as thenApply, thenAccept, thenRun can be used here

    Because it depends on the result of the previous step, it is recommended to use the thenXxx method here instead of the thenXxxAsync asynchronous method

  • At <3>, wait for both order query and address query to complete

  • At <4>, log the exception and rethrow the exception. necessary.

    The log must be recorded. If the exception is not recorded, the exception information will be lost

  • at <5>, whether to rethrow the exception. Optional.

    In some cases, individual interface exceptions are also acceptable, and exceptions may not be thrown, but exceptions must be recorded.

  • At <6>, return the result set of orders and addresses to the user

Guess you like

Origin blog.csdn.net/yuchangyuan5237/article/details/132176213