Summary! Java8 Enhanced Future: CompletableFuture

CompletableFuture is a super-large tool class newly added in Java8. Why is it big? Because on the one hand, it implements the Future interface, and more importantly, it implements the CompletionStage interface. This interface is also newly added in Java8, and CompletionStage has many up to about 40 methods,

通过CompletableFuture提供进一步封装,我们很容易实现Future模式那样的异步调用,例如:

public static Integer cale(Integer para) {
    try {
        Thread.sleep(1000);
 
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return para * para;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
 
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> cale(50));
    System.out.println(future.get());
}

In the above code, the CompletableFuture.supplyAsync() method constructs a CompletableFuture instance. In the supplyAsync() function, it will execute the incoming parameters in a new thread. Here, it will execute the calc() method, and calc The execution of the () method may be relatively slow, but it does not affect the construction speed of the CompletableFuture instance, so supplyAsync() will return immediately, and the CompletableFuture object instance it returns can be used as the contract for this call. In any future occasion, it is used for Get the final calculation result. In CompletableFuture, similar factory methods are as follows:

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)

The supplyAsync() method is used for scenarios that require a return value, such as computing a certain data, and the runAsync() method is used for scenarios without a return value, such as simply executing an asynchronous action.

First of all, it is explained that the methods that have ended with Async can be executed asynchronously. If a thread pool is specified, it will be executed in the specified thread pool. If not specified, it will be executed in ForkJoinPool.commonPool() by default.

     In these two pairs of methods, there is a method that can take over an Executor parameter, which allows us to let Supplier<U> or Runnable work in the specified thread pool. If not specified, the default system public ForkJoinPool.common thread Executed in the pool.

* Streaming call

在前文中我已经简单的提到,CompletionStage的约40个接口为函数式编程做准备的,在这里,就让我们看一下,如果使用这些接口进行函数式的流式API调用

public static Integer cale(Integer para) {
    try {
        Thread.sleep(1000);
 
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return para * para;
}
CompletableFuture<Void> future = CompletableFuture
        .supplyAsync(() -> cale(50))
        .thenApply(i -> Integer.toString(i))
        .thenApply(str -> "\"" + str + "\"")
        .thenAccept(System.out::println);
future.get();

上述代码中,使用supplyAsync()函数执行了一个异步任务,接着连续使用流式调用对任务处理结果进行在加工,直到最后的结果输出:

* Exception handling in CompletableFuture

public static Integer cale(Integer para) {
    try {
        Thread.sleep(1000);
 
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return para * para;
}
CompletableFuture<Void> future = CompletableFuture
        .supplyAsync(() -> cale(50))
        .exceptionally(ex -> {
            System.out.println("ex.toString() = " + ex.toString());
            return 0;
        })
        .thenApply(i -> Integer.toString(i))
        .thenApply(str -> "\"" + str + "\"")
        .thenAccept(System.out::println);
future.get();

* Combine multiple CompletableFutures

CompletableFuture还允许你将多个CompletableFuture进行组合,一种方法是使用thenCompose(),它的签名如下:
public <U>CompletableFuture<U>thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)

CompletableFuture<Void> future = CompletableFuture
        .supplyAsync(() -> cale(50))
        .thenCompose(i -> CompletableFuture
                .supplyAsync(() -> cale(i)))
        .thenApply(i -> Integer.toString(i))
        .thenApply(str -> "\"" + str + "\"")
        .thenAccept(System.out::println);
future.get();

Another method for grouping and multiple CompletableFutures is thenCombine(), which has the following signature:
public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn)

/*方法thenCombine()首先完成当前CompletableFuture和other的执行,
接着,将这两者的执行结果传递给BiFunction(该接口接受两个参数,并有一个返回值),
并返回代表BiFuntion实例的CompletableFuture对象:*/
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> cale(50));
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> cale(25));
 
CompletableFuture<Void> fu = future1.thenCombine(future2, (i, j) -> (i + j))
        .thenApply(str -> "\"" + str + "\"")
        .thenAccept(System.out::println);
fu.get();

 * Implement asynchronous API

public double getPrice(String product) {
    return calculatePrice(product);
}

/**
 * 同步计算商品价格的方法
 *
 * @param product 商品名称
 * @return 价格
 */
private double calculatePrice(String product) {
    delay();
    return random.nextDouble() * product.charAt(0) + product.charAt(1);
}
/**
 * 模拟计算,查询数据库等耗时
 */
public static void delay() {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

* Replace synchronous methods with asynchronous methods

/**
 * 异步计算商品的价格.
 *
 * @param product 商品名称
 * @return 价格
 */
public Future<Double> getPriceAsync(String product) {
    CompletableFuture<Double> futurePrice = new CompletableFuture<>();
    new Thread(() -> {
        double price = calculatePrice(product);
        futurePrice.complete(price);
    }).start();
    return futurePrice;
}

使用异步API 模拟客户端
Shop shop = new Shop("BestShop");
long start = System.nanoTime();
Future<Double> futurePrice = shop.getPriceAsync("my favorite product");
long incocationTime = (System.nanoTime() - start) / 1_000_000;
System.out.println("执行时间:" + incocationTime + " msecs");
try {
    Double price = futurePrice.get();
    System.out.printf("Price is %.2f%n", price);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
long retrievalTime = (System.nanoTime() - start) / 1_000_000;
System.out.println("retrievalTime:" + retrievalTime + " msecs");

//>执行时间:37 msecs
//>Price is 125.79
//>retrievalTime:1055 msecs

* Error handling

The above code, if there are no surprises, will work fine, but what if an error is generated during the price calculation? Unfortunately, in this case you will get a pretty bad result:
the exception for the error will be limited to the view Within the scope of the current thread that calculates the price of the commodity, the thread will eventually be killed,
and this will cause the client waiting for the get method to return the result to be permanently blocked. The
     client can use the overloaded get method, which gives A timeout period, which is a recommended practice!
     In order to let the client know why the store cannot provide the requested item price. We optimize the code, !

/**
 * 异步计算商品的价格.
 *
 * @param product 商品名称
 * @return 价格
 */
public Future<Double> getPriceAsync(String product) {
    CompletableFuture<Double> futurePrice = new CompletableFuture<>();
    new Thread(() -> {
        try {
            double price = calculatePrice(product);
            futurePrice.complete(price);
        } catch (Exception e) {
            //否则就抛出异常,完成这次future操作
            futurePrice.completeExceptionally(e);
        }
    }).start();
    return futurePrice;
}

* Use the factory method supplyAsync to create a CompletableFuture

/**
 * 异步计算商品的价格.
 *
 * @param product 商品名称
 * @return 价格
 */
public Future<Double> getPriceAsync(String product) {
   /* CompletableFuture<Double> futurePrice = new CompletableFuture<>();
    new Thread(() -> {
        try {
            double price = calculatePrice(product);
            futurePrice.complete(price);
        } catch (Exception e) {
            //否则就抛出异常,完成这次future操作
            futurePrice.completeExceptionally(e);
        }
    }).start();
    return futurePrice;*/

    return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}

* Let the code avoid the pain of blocking

案例:最佳价格查询器
private static List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
        new Shop(":LetsSaveBig"),
        new Shop("MyFavoriteShop"),
        new Shop("BuyItAll"));

/**
 * 最佳价格查询器
 *
 * @param product 商品
 * @return
 */
public static List<String> findprices(String product) {
    return shops
            .stream()
            .map(shop -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product)))
            .collect(Collectors.toList());
}

验证findprices的正确性和执行性能
long start = System.nanoTime();
System.out.println(findprices("myPhones27s"));
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("Done in " + duration+" msecs");
/*
[BestPrice price is 197.76, :
LetsSaveBig price is 155.39, 
MyFavoriteShop price is 124.21,
BuyItAll price is 139.23]
Done in 4071 msecs
*/

* Use parallel streams to perform parallel operations on requests

/**
 * 最佳价格查询器(并行流)
 *
 * @param product 商品
 * @return
 */
public static List<String> parallelFindprices(String product) {
    return shops
            .parallelStream()
            .map(shop -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product)))
            .collect(Collectors.toList());
}
/*
[BestPrice price is 201.41, :
LetsSaveBig price is 153.64,
MyFavoriteShop price is 224.65, 
BuyItAll price is 211.83]
Done in 1064 msecs
*/
Pretty good, looks like a simple and efficient idea, parallelizing the query on 4 different stores. The total time for all the operations to complete is just over a second,
Let's try using CompletableFuture, replacing the synchronous calls to different stores in the findprices method with asynchronous calls.

* Use CompletableFuture to initiate asynchronous requests

/**
 * 最佳价格查询器(异步调用实现)
 * @param product 商品
 * @return
 */
public static List<String> asyncFindprices(String product) {
    //使用这种方式,你会得到一个List<CompletableFuture<String>>,
    //列表中的每一个CompletableFuture对象在计算完成后都包含商店的String类型的名称.
    //但是,由于你用CompletableFuture实现了asyncFindprices方法要求返回一个List<String>.
    //你需要等待所有的future执行完毕,将其包含的值抽取出来,填充到列表中才能返回
    List<CompletableFuture<String>> priceFuture = shops
            .stream()
            .map(shop ->CompletableFuture.supplyAsync(() ->
     String.format("%s price is %.2f", shop.getName(), shop.getPrice(product))))
                 .collect(Collectors.toList());
    //为了实现这个效果,我门可以向最初的List<CompletableFuture<String>>
    //施加第二个map操作,对list中的每一个future对象执行join操作,一个接一个地等待他们允许结束,join和get方法
    //有相同的含义,不同的在于join不会抛出任何检测到的异常
    return priceFuture
            .stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
}

[BestPrice price is 187.24, :
LetsSaveBig price is 158.26, 
MyFavoriteShop price is 169.78, 
BuyItAll price is 170.59]
Done in 1061 msecs
The result let us down. We call the new method asynchronously, which is similar to parallel

* Find a better solution

After I increased the number of stores and repeatedly tested in three ways, I found a problem. The performance of parallel streams and asynchronous calls are comparable, and the reasons are the same.
They use the same general thread pool internally, and use a fixed number of threads by default.
The specific number of threads depends on the value returned by Runtime.getRuntime.availableProcessors(), however,
.CompletableFuture has certain advantages because it allows you to configure the executor,
In particular, the size of the thread pool allows it to be configured in a way that suits the needs of the application and meets the requirements of the program, which the parallel streaming API cannot provide.
private static final Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100));
/**
 * 最佳价格查询器(异步调用实现,自定义执行器)
 *
 * @param product 商品
 * @return
 */
public static List<String> asyncFindpricesThread(String product) {
    List<CompletableFuture<String>> priceFuture = shops
            .stream()
            .map(shop -> CompletableFuture.supplyAsync(
() -> shop.getName() + " price is " + shop.getPrice(product), executor))
            .collect(Collectors.toList());
    return priceFuture
            .stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
}

After testing, it takes more than 1 second to process 5 stores, and it takes more than 1 second to process 9 stores.

Parallelism - Using Streams or CompletableFutures?

So far, we already know that there are two ways to parallelize the collection, either convert it into a parallel stream and use operations such as map to do work, or enumerate each element in the collection, create a new thread, and execute it in CompletableFuture The latter provides more flexibility, you can adjust the thread pool size, both can help you ensure that the overall computer does not block because threads are all waiting for I/O. Our recommendations for using these APIs as follows:

  1.  If you are doing computationally intensive operations and there is no I/O, the Stream interface is recommended because it is simple to implement and may be the most efficient.
  2.  Conversely, if your parallel unit of work also involves waiting for I/O operations (including network connection waits). Then using CompletableFuture is more flexible, you can, as discussed earlier, depend on wait/compute, or W/C The ratio sets the number of threads to be used,

In the previous article , you can take a look at < summary, summary!!!! java callback, future >

Creation depends on wisdom, and life depends on common sense; common sense without wisdom is called mediocrity, wisdom without common sense is called clumsy. Wisdom is the most powerful of all powers, the only consciously alive power in the world. - Gorky

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324913222&siteId=291194637