Learn Java8 Together--CompletableFuture

Synchronous Asynchronous

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

If asynchronous programming is used, nine other channels are available and the program throughput increases.That is, to make full use of CPU resources to keep them busy, asynchronous programming is undoubtedly one way to keep them busy.

CompletableFuture

Before CompletableFuture comes out, we can use the Future interface to do asynchronous programming. Future works with the thread pool, which handles tasks to the thread pool and uses the Future.get() method to get results after processing in the thread pool. Future.get() can be interpreted as a callback operation, and we can do other things before callback.

The following example is used to simulate the scene of a loan book:

  1. Xiao Ming goes to the library to borrow books
  2. Librarian Find Books (Asynchronous Operation)
  3. Xiao Ming is waiting while playing with his mobile phone
  4. Xiao Ming gets the book
public class FutureTest extends TestCase {
    // Declare a thread pool
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
            5,
            0,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());

    public void testBook() {
        String bookName = "<Gone with the Wind";
        System.out.println("Xiao Ming goes to the library to borrow books");
        Future<String> future = threadPoolExecutor.submit(() -> {
            // Simulate the librarian's time spent finding books
            long minutes = (long) (Math.random() * 10) + 1;
            System.out.println("The librarian spent" + minutes + "Minutes, books found" + bookName);
            Thread.sleep((long) (Math.random() * 2000));
            return bookName;
        });
        // Do something else while waiting
        this.playPhone();
        try {
            String book = future.get();
            System.out.println("Xiao Ming has got the book" + book);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    private void playPhone() {
        System.out.println("Xiao Ming is playing with his mobile phone and waiting for a Book");
    }

}

This is a typical Future usage where the future.get() method is blocked and the program runs on this line. It is impossible for our program to wait forever. We can use future.get(long timeout,TimeUnit unit) method, given a wait time, throws a TimeoutException exception if the wait time is exceeded or the data is not received, we can catch the exception and then handle the exception.

Now suppose Xiao Ming waits up to 2 minutes, then the code can be written as follows:

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

Now there is such a situation, suppose the librarian finds the book, and then he also needs to give it to the assistant, who can enter the book information, and then give the book to Xiao Ming after the information is recorded.The assistant entry process is also asynchronous, that is, we want to achieve multiple functions such as asynchronous pipelining.

Future can be found to be very difficult to handle multi-asynchronous pipelining, and CompletableFuture comes in handy when it comes to pipelining, like the Stream corresponding to Collection.

Let's take a look at the basic usage of CompletableFuture, and we'll use the example above to implement it:

public class CompletableFutureTest extends TestCase {

    public void testBook() {
        String bookName = "<Gone with the Wind";
        System.out.println("Xiao Ming goes to the library to borrow books");
        CompletableFuture<String> future = new CompletableFuture<>();
        new Thread(() -> {
            // Simulate the librarian's time spent finding books
            long minutes = (long) (Math.random() * 10) + 1;
            System.out.println("The librarian spent" + minutes + "Minutes, books found" + bookName);
            try {
                Thread.sleep((long) (Math.random() * 2000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            future.complete(bookName);
        }).start();
        // Do something else while waiting
        this.playPhone();
        try {
            String book = future.get();
            System.out.println("Xiao Ming has got the book" + book);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    private void playPhone() {
        System.out.println("Xiao Ming is playing with his mobile phone and waiting for a Book");
    }

}

Where future.complete(bookName); means to return the result and get the data where future.get() is called.

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

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // Simulate the librarian's time spent finding books
    long minutes = (long) (Math.random() * 10) + 1;
    System.out.println("The librarian spent" + minutes + "Minutes, found the book Gone with the Wind");
    try {
        Thread.sleep((long) (Math.random() * 2000));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "<Gone with the Wind";
});

If you do not need to return a result, you can use the CompletableFuture.runAsync(Runnable) method:

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

Next, we use CompletableFuture to fulfill the above requirements: Once the librarian finds the book, he or she also needs to hand it in to the assistant to let the assistant enter the book information.

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

public void testBook3() {
    String bookName = "<Gone with the Wind";
    System.out.println("Xiao Ming goes to the library to borrow books");
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        // Simulate the librarian's time spent finding books
        long minutes = (long) (Math.random() * 10) + 1;
        System.out.println("The librarian spent" + minutes + "Minutes, books found"+bookName);
        try {
            Thread.sleep((long) (Math.random() * 2000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return bookName;
    })
            // thenCompose, join the second asynchronous task
            .thenCompose((book/*The parameter here is the first asynchronous return result*/) -> CompletableFuture.supplyAsync(()-> {
        System.out.println("Assistant Enter Book Information");
        return book;
    }));
    // Do something else while waiting
    this.playPhone();
    try {
        String book = future.get();
        System.out.println("Xiao Ming has got the book" + book);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

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

The nApply (Function) means that the result returned by CompletableFuture is further processed and a new result is returned. Its parameter uses Function, meaning that a lambda expression can be used, which provides a parameter and then requires a return result.

public void testThenApply() throws ExecutionException, InterruptedException {
    int i = 0;
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> i + 1)
    // Converts the added result to a string
    // v is the result of i+1 above
    // Equivalent to:.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 a result, you can use thenAccept (Consumer<? Super T> action)

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

The use of thenCompose() is as follows:

completableFuture1.thenCompose((completableFuture1_result) -> completableFuture2)

This code means to take the result returned in completableFuture1 into completableFuture2 and execute it, then return the result in completableFuture2. Here 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

The nCombine() method combines the results of two CompletableFuture tasks

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("First CompletableFuture Result:" + result1);
                System.out.println("The second CompletableFuture Result:" + result2);
                return result1 + result2;
            });
    Integer total = future.get();
    System.out.println("The sum:" + total);
}

Print:

First CompletableFuture result: 1
 Second CompletableFuture result:2
 Sum up: 3

CompletableFuture.join()

Assuming you have a set of CompletableFuture objects, you now need to complete all these CompletableFuture tasks before you can do something else.For this need, we can use the CompletableFuture.join() method.

public void testJoin() {
    List<CompletableFuture> futures = new ArrayList<>();
    System.out.println("100 Start of the rice race");
    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 + "Number one reaches the end point in time:" + (10 + v) + "second");
        }));
    }
    CompletableFuture<Double>[] futureArr = futures.toArray(new CompletableFuture[futures.size()]);
    CompletableFuture.allOf(futureArr).join();
    System.out.printf("All runners reach the end point");
}

Print:

Start of the 100-meter race
 Player 3 arrives at the end point in 16 seconds
 Player 1 arrives at the end point in 15 seconds
 Player 2 arrives at the end point in 11 seconds
 Player 5 arrives at the end point in 13 seconds
 Player 4 arrives at the end point in 15 seconds
 Player 8 arrives at the end point in 10 seconds
 Player 6 arrives at the end point in 18 seconds
 Player 10 arrives at the end point in 12 seconds
 Player 7 arrives at the end point in 19 seconds
 Player 9 arrives at the end point in 18 seconds
 All runners reach the end point

whenComplete()

If you need to do something after the task has been processed, you can use whenComplete(BiConsumer) or whenCompleteAsync(BiConsumer)

String bookName = "<Gone with the Wind";
CompletableFuture<String> future = CompletableFuture.
        supplyAsync(() -> {System.out.println("The librarian starts looking for books");return bookName;})
        .thenApply((book) -> {System.out.println("Find the book and the assistant starts typing in the information"); return book;})
        .whenCompleteAsync(((book, throwable) -> {
            System.out.println("When the assistant has finished entering the information, he informs Xiao Ming to pick up the book.");
}));
String book = future.get();
System.out.println("Xiao Ming gets the book" + book);

Print:

The librarian starts looking for books
 Find the book and the assistant starts typing in the information
 When the assistant has finished entering the information, he informs Xiao Ming to pick up the book.
Xiao Ming gets the book Gone with the Wind

exception handling

Processing exceptions is usually a two-step process. The first step throws an exception, and the second step catches an exception. First, let's see how CompletableFuture throws an exception.

CompletableFuture throws exceptions in two ways. The first way is that if CompletableFuture is a directly new object, it must use future.completeExceptionally(e) to throw an exception. If it throws a new RuntimeException (e), the caller cannot catch the exception.

CompletableFuture<String> future = new CompletableFuture<>();
new Thread(()->{
    String value = "http://";
    try {
        // Simulation error, giving a nonexistent ENCODE
        value = URLEncoder.encode(value, "UTF-888");
        future.complete(value);
    } catch (UnsupportedEncodingException e) {
        // future handles exceptions
        future.completeExceptionally(e);
        // !!This method caller cannot catch exceptions
        // throw new RuntimeException(e);
    }
}).start();

Second, if the CompletableFuture object is created by a factory method (such as CompletableFuture.supplyAsync()), throw the new RuntimeException (e) directly, because the method encapsulated by supplyAsync() does try...catch processing inside it

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    String value = "http://";
    try {
        // Simulation error, giving a nonexistent ENCODE
        value = URLEncoder.encode(value, "UTF-88");
        return value;
    } catch (UnsupportedEncodingException e) {
        // No, you can throw
        //future.completeExceptionally(e);
        throw new RuntimeException(e);
    }
});

Next, let's look at how to catch exceptions, which are divided into two types.

First, capture in catch:

try {
    future.get();
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    // Catch exception 1, first come here
    System.out.println("Catch Exception 1,msg:" + e.getMessage());
}

Second, captured in the future.whenCompleteAsync() or future.whenComplete() methods:

future.whenCompleteAsync((value, e)->{
    if (e != null) {
        // Catch exception 2, this will also print
        System.out.println("Catch Exception 2, msg:" + e.getMessage());
    } else {
        System.out.println("Return results:" + value);
    }
});

Summary

The emergence of CompletableFuture compensates for the shortcomings of the Future interface in some areas, such as event monitoring, multitask merging, pipelining, and so on.CompletableFuture, along with lambda expressions, gives developers a more versatile choice in asynchronous programming.

Tags: Programming Mobile Lambda

Posted on Wed, 18 Mar 2020 22:58:45 -0400 by jaddy