Java — good practices and recommendations

101 Async task with Completable Future

Martin Jonovski
7 min readDec 7, 2016
Luis Llerena

“It is not enough for code to work.” ― Robert C. Martin

Singular’s backend team participated in this year’s Java2Days annual conference. The conference took place in Sofia, Bulgaria, where new trends and features in the Java community were promoted and discussed. Java2Days recommends practices on how to write good code, that is scalable, responsive, fast and of course, functional. Alongside with Streams and Lambdas, which are new in Java 1.8, one of the important features that were mentioned is CompletableFuture, which is used for asynchronous programming.

This article will provide insight in the technical implementation of CompetableFuture, its features, the get method and tips for error handling.

Note: Further in this article, the term Future will refer to a big computation task.

Why should you use Async Task?

Waiting is the most common reason for a user to stop using a certain web service.

We need to start thinking asynchronously. How?

If you have many tasks that don’t depend on your main thread, but you need them done, you should start working with async task.

The most important thing in async programming is the schedule of the tasks. You need to prioritize and determine the order in which you want them executed. Furthermore, you can use the result from one job to complete another or some kind of event can be the initiator for the job etc.

The tool for asynchronous programming in Java is the CompletableFuture, which is a type of future that handles three basic paradigms in the asynchronous programming:

  • Order manually the completion of two or more tasks
  • Error handling
  • Canceling or forcing completion of tasks, depending on the error result

Technical Implementation

The CompletableFuture implements the Completion Stage interface. The javadoc concisely explains what the Completion Stage is:

“A stage of a possibly asynchronous computation, that performs an action or computes a value when another Completion Stage completes. A stage completes upon termination of its computation, but this may in turn trigger other dependent stages.”

The creation of a CompletableFuture in Java is pretty straightforward and it’s done by using its constructor or by using the factory methods by creating the future and supplying the task right away by calling the method supplyAsync(). In the example below we use lambdas and inline functions, which are new in Java 8.

CompletableFuture completableFuture = new CompletableFuture();
CompletableFuture completableFuture = CompletableFuture.supplyAsync( ( ) -> {// big computation taskreturn "1";} );

Completable futures can be managed by themselves, as mentioned further in this article, or can be managed by an executor, which will give the order of execution for each of the futures. To create an Executor just run the following code:

Executor e = Executors.newFixedThreadPool(nThreads);

Where the variable nThreads is an integer representing the number of running threads.

Getting the Result

The get method for the CompletableFuture throws some checked exceptions, namely ExecutionException (encapsulating an exception that occurred during a computation) and InterruptedException (an exception signifying that a thread executing a method was interrupted).

For this method, you can specify the time that you will wait for its execution to be completed.

System.out.println( “get in 3 seconds “ + completableFuture.get( 3, TimeUnit.SECONDS ) );

If the process is finished within x units of time (specified as an argument to the get method) and tries to return the computed value if available, else the process is not completed and the method throws an exception of type TimeOutException. If the result that should be returned is known, this can be used as an assertion whether the future has finished correctly. This is done by invoking the method CompletedFuture, which we need to provide with the expected result.

completableFuture.completedFuture(“1”);String result = completableFuture.get();assertEquals(“1”, result);

Features

1. Completion

With CompletableFuture, we can have a future that is running for a long time and another future that forces the first one to complete. In this scenario, there is a process that runs forever, so if we want to complete another process we will give the first one some time and after that we execute whatever needed to be executed, regardless of the first one. After that, the process will go on, but after the specified time the second future will be completed and then the first one will resume.

CompletableFuture completableFutureToBeCompleted2 = CompletableFuture.supplyAsync(() -> {for (int i = 0; i < 10; i--) {System.out.println("i " + i);}return 10;});CompletableFuture completor = CompletableFuture.supplyAsync(() -> {System.out.println("completing the other");completableFutureToBeCompleted2.complete(222);return 10;});System.out.println(completor.get());

2. Joining

Another great use of the futures in Java is joining two processes, meaning, that each of them will be executed asynchronously and after getting their results another process will start. This is provided with the methods thenApply() and thenCompose(). In the following example the method thenComposeAsync() demonstrates how we can give an order for execution of certain processes, which will be joined.

CompletableFuture.runAsync(() -> {System.out.println("Task 1. Thread: " + Thread.currentThread().getId());}, e).thenComposeAsync((Void unused) -> {System.out.println("Task 1 1/2. Thread: " + Thread.currentThread().getId());return CompletableFuture.runAsync(() -> {System.out.println("Task 2. Thread: " + Thread.currentThread().getId());}, e);}, e).join();

Another way for Joining two processes is by using the thenCombine() method. Let’s see the following scenario: For implementing a log in system, the application must verify the login credentials for a certain user and check if the country from where the user logs in is on a list of allowed countries. These two processes need to result in a successful log in.

So we need two CompletableFutures, one to check the credentials and another to check the countries. ThenCombine() will combine the results of these two futures and start a new future which will complete the process of logging in. The result is given in a lambda function. The function thenCombine() has two parameters: The futures that will be combined and the result of the two futures.

CompletableFuture credentials = Login( login, password );
CompletableFuture country = checkCountry( country );
CompletableFuture logIn = credentials.thenCombine( checkCountry,
( cust, shop ) -> completeLogin( cust, shop ) );

The functions login(),completeLogin() and checkCountry() are given without implementation, to serve as an example. While thenCompose() is used to chain one future dependent on the other, thenCombine combines two independent futures when they are both done.

3. Parallel Execution

When we want several futures to be completed and if they are all independent one to another, we can use the method allOf(). This method returns as a result a Completable Future of type Void. The execution of this method will result in parallel execution of the three processes. The execution may seem parallel, but it is not. It is given a certain amount of processor time for each one of the threads and by doing so, the threads will be executed in no particular order, for as long as the processor allows it.

The result of the parallel execution can be an input for another completable future or can be used further in the application.

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {System.out.println("Hi");return "Hi";});CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {System.out.println("Hello");return "Hello";});CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {System.out.println("World");return "World";});CompletableFuture<Void> combinedFuture= CompletableFuture.allOf(future1, future2, future3);combinedFuture.get();

Opposed to the allOf() method the class has a method anyOf() which will get the result of the first future that finishes and will assign it to the new future.

4. Waiting for both CompletableFutures to complete

If instead of producing new CompletableFuture combining both results we simply want to be notified when they finish, we can use thenAcceptBoth()/runAfterBoth() family of methods (Async variations are available as well). They work similarly to thenAccept() and thenRun() but wait for two futures instead of one.

customerFuture.thenAcceptBoth(shopFuture, (cust, shop) -> {final Route route = findRoute(cust, shop);});

I hope I’m wrong but maybe some of you are asking themselves a question: why can’t I simply block on these two futures? Meaning, we can tell the application to first run one of them, then the second and the one resulting from the first two processes.

Well, of course you can. But the whole point of CompletableFuture is to allow asynchronous, event driven programming model instead of blocking and eagerly waiting for result. So functionally two code snippets above are equivalent, but the latter unnecessarily occupies one thread of execution.

Error Handling

Error handling refers to the actions taken after a process has finished unsuccessfully. The process may produce an exception, which can be caught and dealt with.

There are two ways in which Compeltable Future allows error handling. The first one is inside the future, but this time instead of runAsync() the method handle() is used. This method provides the option for adding an error handler inside in the function.

CompletableFuture<Integer> safe = future.handle((ok, ex) -> {if (ok != null) {return Integer.parseInt(ok);} else {log.warn("Problem", ex);return -1;}});

The other way for handling errors is using the method exceptionally(), in which we can define the steps that we want to be taken if the execution of the future results with an exception. In this example we set the value of “x” to 0, if an exception is thrown.

CompletableFuture completableFutureException = CompletableFuture.supplyAsync( ( ) -> {
return 10 /0;
} );

CompletableFuture fallback = completableFutureException.exceptionally( x -> 0 );
System.out.println( fallback.get() );

Check the DEMO for better comprehension of the above explanations.

Follow our blog for more tech updates as we tackle new features of Java in the future.

--

--