Let's learn Java8 (9) - CompletableFuture

Synchronous Asynchronous

Computer technology has developed rapidly, both in terms of software and hardware. The CPU of the computer is also being updated, and a powerful CPU can undertake more tasks. If the program has been using synchronous programming, it will waste CPU resources. For example, a CPU has 10 channels. If all programs use one channel, then the remaining 9 channels are idle, and these 9 channels are wasted.

If asynchronous programming is used, then the other 9 channels can be utilized, and the throughput of the program also increases. That is to say, it is necessary to make full use of CPU resources to keep it busy, and asynchronous programming is undoubtedly a way to keep it busy.

CompletableFuture

Before CompletableFuture comes out, we can use the Future interface for asynchronous programming. Future works with the thread pool. It hands the task to the thread pool. After the thread pool is processed, the result is obtained through the Future.get() method. Future.get( ) can be understood as a callback operation, and we can do other things before the callback.

The following example is used to simulate the scene of Xiaoming borrowing books:

  1. Xiao Ming went to the library to borrow books
  2. Librarian looking for books (asynchronous operation)
  3. Xiao Ming waits while playing with his mobile phone
  4. Xiao Ming got the book
public class FutureTest extends TestCase {
    // 申明一个线程池
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
            5,
            0,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());

    public void testBook() {
        String bookName = "《飘》";
        System.out.println("小明去图书馆借书");
        Future<String> future = threadPoolExecutor.submit(() -> {
            // 模拟图书管理员找书花费时间
            long minutes = (long) (Math.random() * 10) + 1;
            System.out.println("图书管理员花费了" + minutes + "分钟,找到了图书" + bookName);
            Thread.sleep((long) (Math.random() * 2000));
            return bookName;
        });
        // 等待过程中做其他事情
        this.playPhone();
        try {
            String book = future.get();
            System.out.println("小明拿到了图书" + book);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    private void playPhone() {
        System.out.println("小明在玩手机等待图书");
    }

}

This is a typical way of using Future, in which the future.get()method is blocked, the program will stay in this line, and our program cannot wait forever. At this time, we can use the future.get(long timeout, TimeUnit unit)method to give a waiting time. If the waiting time is exceeded, there is still no When the data is obtained, a TimeoutException is thrown, we can catch this exception, and then deal with the abnormal situation.

Now suppose that Xiao Ming waits at most 2 minutes, then the code can be written as follows:

String book = future.get(2, TimeUnit.MINUTES);

Now there is such a situation, assuming that after the librarian finds the book, he needs to hand it over to the assistant, let the assistant enter the book information, and then hand over the book to Xiao Ming after the information is recorded. The process of assistant input is also asynchronous, that is to say, we need to implement multiple functions such as asynchronous pipelines.

It can be found that it is very difficult for Future to handle multiple asynchronous pipelines. At this time, CompletableFuture comes in handy. CompletableFuture has its own pipeline feature, just like the Stream corresponding to Collection.

Let's take a look at the basic usage of CompletableFuture. We will use CompletableFuture to implement the above example:

public class CompletableFutureTest extends TestCase {

    public void testBook() {
        String bookName = "《飘》";
        System.out.println("小明去图书馆借书");
        CompletableFuture<String> future = new CompletableFuture<>();
        new Thread(() -> {
            // 模拟图书管理员找书花费时间
            long minutes = (long) (Math.random() * 10) + 1;
            System.out.println("图书管理员花费了" + minutes + "分钟,找到了图书" + bookName);
            try {
                Thread.sleep((long) (Math.random() * 2000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            future.complete(bookName);
        }).start();
        // 等待过程中做其他事情
        this.playPhone();
        try {
            String book = future.get();
            System.out.println("小明拿到了图书" + book);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    private void playPhone() {
        System.out.println("小明在玩手机等待图书");
    }

}

Which future.complete(bookName);means that the result is returned, and then the data can be obtained where future.get() is called.

CompletableFuture also provides a static method CompletableFuture.supplyAsync(Supplier)to quickly create a task, and the parameter Supplier is used to return the task result.

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 模拟图书管理员找书花费时间
    long minutes = (long) (Math.random() * 10) + 1;
    System.out.println("图书管理员花费了" + minutes + "分钟,找到了图书《飘》");
    try {
        Thread.sleep((long) (Math.random() * 2000));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "《飘》";
});

If you don't need to return the result, you can use the CompletableFuture.runAsync(Runnable)method:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> System.out.println("running"))

Next, we use CompletableFuture to complete the requirements mentioned above:图书管理员找到书本之后,还需要交给助理,让助理录入图书信息

We need to use CompletableFuture.thenCompose(Function)the method, the usage is as follows

public void testBook3() {
    String bookName = "《飘》";
    System.out.println("小明去图书馆借书");
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        // 模拟图书管理员找书花费时间
        long minutes = (long) (Math.random() * 10) + 1;
        System.out.println("图书管理员花费了" + minutes + "分钟,找到了图书"+bookName);
        try {
            Thread.sleep((long) (Math.random() * 2000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return bookName;
    })
            // thenCompose,加入第二个异步任务
            .thenCompose((book/*这里的参数是第一个异步返回结果*/) -> CompletableFuture.supplyAsync(()-> {
        System.out.println("助理录入图书信息");
        return book;
    }));
    // 等待过程中做其他事情
    this.playPhone();
    try {
        String book = future.get();
        System.out.println("小明拿到了图书" + book);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

thenApply(),thenAccept(),thenCompose(),thenCombine()

thenApply(Function)It means to further process the result returned by CompletableFuture, and then return a new result. Its parameter is a Function, which means that a lambda expression can be used, the expression will provide a parameter, and then a return result is required.

public void testThenApply() throws ExecutionException, InterruptedException {
    int i = 0;
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> i + 1)
    // 将相加后的结果转成字符串
    // v就是上面i+1后的结果
    // 等同于:.thenApply((v) -> String.valueOf(v))
            .thenApply(String::valueOf);
    String str = future.get();
    System.out.println("String value: " + str);
}

If you don't need to return the result, you can usethenAccept(Consumer<? super T> action)

The difference between thenApply() and thenAccept() is that thenApply provides parameters and requires a return value, while thenAccept only provides parameters and does not require a return value.

thenCompose()The usage is as follows:

completableFuture1.thenCompose((completableFuture1_result) -> completableFuture2)

The meaning of this code is to bring the result returned in completableFuture1 into completableFuture2 for execution, and then return the result in completableFuture2. The following is a simple example:

public void testThenCompose() throws ExecutionException, InterruptedException {
        int i=0;
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> i + 1)
                .thenCompose((j) -> CompletableFuture.supplyAsync(() -> j + 2));
        Integer result = future.get();
    }

Print:

result:3

thenCombine()The method is to combine the two CompletableFuture task results

public void testThenCombine() throws ExecutionException, InterruptedException {
    int i=0;
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> i + 1)
            .thenCombine(CompletableFuture.supplyAsync(() -> i + 2), (result1, result2) -> {
                System.out.println("第一个CompletableFuture结果:" + result1);
                System.out.println("第二个CompletableFuture结果:" + result2);
                return result1 + result2;
            });
    Integer total = future.get();
    System.out.println("总和:" + total);
}

Print:

第一个CompletableFuture结果:1
第二个CompletableFuture结果:2
总和:3

CompletableFuture.join()

Suppose there is a set of CompletableFuture objects, and now all of these CompletableFuture tasks need to be executed before continuing to do something. For this requirement, we can use the CompletableFuture.join()method.

public void testJoin() {
    List<CompletableFuture> futures = new ArrayList<>();
    System.out.println("100米跑步比赛开始");
    for (int i = 0; i < 10; i++) {
        final int num = i + 1;
        futures.add(CompletableFuture.runAsync(() -> {
            int v =  (int)(Math.random() * 10);
            try {
                Thread.sleep(v);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(num + "号选手到达终点,用时:" + (10 + v) + "秒");
        }));
    }
    CompletableFuture<Double>[] futureArr = futures.toArray(new CompletableFuture[futures.size()]);
    CompletableFuture.allOf(futureArr).join();
    System.out.printf("所有选手到达终点");
}

Print:

100米跑步比赛开始
3号选手到达终点,用时:16秒
1号选手到达终点,用时:15秒
2号选手到达终点,用时:11秒
5号选手到达终点,用时:13秒
4号选手到达终点,用时:15秒
8号选手到达终点,用时:10秒
6号选手到达终点,用时:18秒
10号选手到达终点,用时:12秒
7号选手到达终点,用时:19秒
9号选手到达终点,用时:18秒
所有选手到达终点

whenComplete()

If you need to do some processing after the task has been processed, you can use whenComplete(BiConsumer)orwhenCompleteAsync(BiConsumer)

String bookName = "《飘》";
CompletableFuture<String> future = CompletableFuture.
        supplyAsync(() -> {System.out.println("图书管理员开始找书");return bookName;})
        .thenApply((book) -> {System.out.println("找到书本,助理开始录入信息"); return book;})
        .whenCompleteAsync(((book, throwable) -> {
            System.out.println("助理录入信息完毕,通知小明来拿书");
}));
String book = future.get();
System.out.println("小明拿到书" + book);

Print:

图书管理员开始找书
找到书本,助理开始录入信息
助理录入信息完毕,通知小明来拿书
小明拿到书《飘》

exception handling

The processing of exceptions is usually divided into two steps, the first step throws exceptions, and the second step catches exceptions. First, let's look at how CompletableFuture throws exceptions.

There are two ways for CompletableFuture to throw exceptions. The first way is that if CompletableFuture is an object that is directly new, it must use future.completeExceptionally(e)throwing exceptions. If the throw new RuntimeException(e);method throws exceptions, the caller cannot catch them.

CompletableFuture<String> future = new CompletableFuture<>();
new Thread(()->{
    String value = "http://";
    try {
        // 模拟出错,给一个不存在的ENCODE
        value = URLEncoder.encode(value, "UTF-888");
        future.complete(value);
    } catch (UnsupportedEncodingException e) {
        // future处理异常
        future.completeExceptionally(e);
        // !!此方式调用者无法捕获异常
        // throw new RuntimeException(e);
    }
}).start();

In the second way, if the CompletableFuture object is created by a factory method (eg CompletableFuture.supplyAsync()), it can be done directly throw new RuntimeException(e), because the method encapsulated by supplyAsync() does a try...catch process inside

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    String value = "http://";
    try {
        // 模拟出错,给一个不存在的ENCODE
        value = URLEncoder.encode(value, "UTF-88");
        return value;
    } catch (UnsupportedEncodingException e) {
        // 这样不行,可以throw
        //future.completeExceptionally(e);
        throw new RuntimeException(e);
    }
});

Next, let's take a look at how to catch exceptions. There are two types of exceptions.

The first, in catch:

try {
    future.get();
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    // 捕获异常1,首先会到这里来
    System.out.println("捕获异常1,msg:" + e.getMessage());
}

Second, capture in future.whenCompleteAsync() or future.whenComplete() method:

future.whenCompleteAsync((value, e)->{
    if (e != null) {
        // 捕获异常2,这里也会打印
        System.out.println("捕获异常2, msg:" + e.getMessage());
    } else {
        System.out.println("返回结果:" + value);
    }
});

summary

The emergence of CompletableFuture makes up for the deficiencies of the Future interface in some places, such as event monitoring, multi-task merging, and pipeline operations. At the same time, CompletableFuture cooperates with lambda expressions to make it more convenient for developers to use, so that developers have one more choice in asynchronous programming.

{{o.name}}
{{m.name}}

Guess you like

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