Java Multithreading | Part 6
Published on July 23, 2024 • 6 min read

Exploring Java's Future and CompletableFuture for Multithreading
In this post, we'll dive into Java's Future
and CompletableFuture
classes, exploring how they can be used to manage asynchronous tasks in a multithreaded environment.
Using Future
in Java for Asynchronous Computation
The Future
interface in Java allows us to write asynchronous programs, where tasks can be executed in separate threads. Here's an overview of using Future
with Runnable
and Callable
tasks, along with key methods and outputs.
Example Code
package multithreading;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class FutureExample {
public static void main(String[] args) {
ExecutorService taskExecutor = Executors.newFixedThreadPool(2);
// Runnable task without a return value
Future<?> futureObj = taskExecutor.submit(() -> {
System.out.println("Runnable task Executed by: " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
try {
Object returnValue = futureObj.get(); // Blocking call
System.out.println("Return value should be null: " + (returnValue == null));
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
System.out.println("Methods in Future Interface");
System.out.println("isCancelled() ==> " + futureObj.isCancelled());
System.out.println("isDone() ==> " + futureObj.isDone());
// Runnable task with a return object
List<Integer> inp = new ArrayList<>();
Future<List<Integer>> futureObj2 = taskExecutor.submit(() -> {
System.out.println("Another runnable task Executed by: " + Thread.currentThread().getName());
inp.add(100);
System.out.println("Runnable task with a return object");
}, inp);
try {
List<Integer> returnValue = futureObj2.get();
System.out.println("Input List: " + inp);
System.out.println("Input List -- : " + returnValue);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
// Callable task with a return value
Future<List<Integer>> futureObj3 = taskExecutor.submit(() -> {
List<Integer> out = new ArrayList<>();
System.out.println("Another callable task Executed by: " + Thread.currentThread().getName());
out.add(500);
System.out.println("Callable task with a return object");
return out;
});
try {
List<Integer> returnValue2 = futureObj3.get();
System.out.println("Output List -- : " + returnValue2);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
taskExecutor.shutdown();
}
}
Key Points
-
Runnable Task:
- Executes asynchronously with no return value.
- The
Future
object can still provide status information (e.g.,isCancelled()
,isDone()
).
Output:
Runnable task Executed by: pool-1-thread-1 Return value should be null: true Methods in Future Interface isCancelled() ==> false isDone() ==> true
-
Runnable Task with Return Object:
- Allows passing a result object alongside task execution.
Output:
Another runnable task Executed by: pool-1-thread-2 Runnable task with a return object Input List: [100] Input List -- : [100]
-
Callable Task:
- Executes asynchronously and returns a result.
- Suitable for tasks that compute a value and may throw exceptions.
Output:
Another callable task Executed by: pool-1-thread-1 Callable task with a return object Output List -- : [500]
Runnable vs. Callable in Java: A Quick Comparison
The Runnable
Interface
- Method:
run()
- Return Type:
void
- Checked Exceptions: Cannot throw
- Usage: Simple tasks without a return value
Example
Runnable runnableTask = () -> {
System.out.println("Runnable executed by: " + Thread.currentThread().getName());
};
new Thread(runnableTask).start();
Output:
Runnable executed by: main
The Callable
Interface
- Method:
call()
- Return Type: Generic (e.g.,
Integer
) - Checked Exceptions: Can throw
- Usage: Tasks that return a result or may throw exceptions
Example
Callable<Integer> callableTask = () -> {
System.out.println("Callable executed by: " + Thread.currentThread().getName());
return 42;
};
Future<Integer> future = Executors.newSingleThreadExecutor().submit(callableTask);
Integer result = future.get();
System.out.println("Callable result: " + result);
Output:
Callable executed by: pool-1-thread-1
Callable result: 42
Key Differences
-
Return Values:
Runnable
: No return value.Callable
: Returns a value.
-
Exception Handling:
Runnable
: Cannot throw checked exceptions.Callable
: Can throw checked exceptions.
-
Use Cases:
Runnable
: For simple tasks without a need for a return value.Callable
: For tasks requiring a result or exception handling.
Choose Runnable
for simple, non-returning tasks and Callable
when you need a result or need to handle exceptions.
Using CompletableFuture
in Java for Asynchronous Computation
CompletableFuture
is a more flexible and powerful class that supports both manually completed and chained asynchronous computations.
Simple Asynchronous Task
With CompletableFuture
, tasks can be run asynchronously using supplyAsync()
:
CompletableFuture<String> asyncTask1 = CompletableFuture.supplyAsync(() -> {
return "Task completed by: " + Thread.currentThread().getName();
}, taskExecutor);
If no executor is passed, the default ForkJoinPool
is used.
- Advantages: Automatically uses the
ForkJoinPool
if no executor is provided, simplifying resource management. - Limitations: Can lead to unexpected thread management behaviors if not carefully controlled.
Output:
Task completed by: pool-1-thread-2
Chaining with thenApply
and thenApplyAsync
You can chain further actions using methods like thenApply()
and thenApplyAsync()
. The latter executes the next stage asynchronously:
CompletableFuture<String> asyncTask2 = asyncTask1.thenApply(val -> {
return val + " processed by thenApply on thread: " + Thread.currentThread().getName();
});
- thenApply: Executes the function in the same thread that completes the previous stage.
- thenApplyAsync: Executes the function asynchronously, using the thread pool or a provided executor.
Output:
Task completed by: pool-1-thread-2 processed by thenApply on thread: pool-1-thread-2
Composing with thenCompose
and thenComposeAsync
For tasks that depend on the results of previous tasks, thenCompose()
and thenComposeAsync()
are useful:
CompletableFuture<String> asyncTask3 = asyncTask2.thenCompose(val -> {
return CompletableFuture.supplyAsync(() -> val + " continued in thenCompose by: " + Thread.currentThread().getName());
});
- thenCompose: Flattens nested
CompletableFuture
s into a single stage. - thenComposeAsync: Similar to
thenCompose
, but runs asynchronously.
Output:
Task completed by: pool-1-thread-2 processed by thenApply on thread: pool-1-thread-2 continued in thenCompose by: pool-1-thread-1
Consuming Results with thenAccept
and thenAcceptAsync
To consume the result without returning a new value, use thenAccept()
or thenAcceptAsync()
:
CompletableFuture<Void> asyncTask4 = asyncTask3.thenAccept(val -> {
System.out.println("Result consumed: " + val);
});
- thenAccept: Executes a
Consumer
upon task completion. - thenAcceptAsync: Asynchronously executes the
Consumer
.
Output:
Result consumed: Task completed by: pool-1-thread-2 processed by thenApply on thread: pool-1-thread-2 continued in thenCompose by: pool-1-thread-1
Combining Tasks with thenCombine
and thenCombineAsync
To combine results from multiple tasks, thenCombine()
and thenCombineAsync()
are used:
CompletableFuture<String> asyncTask6 = CompletableFuture.supplyAsync(() -> "Task 1", taskExecutor);
CompletableFuture<String> asyncTask7 = CompletableFuture.supplyAsync(() -> "Task 2", taskExecutor);
CompletableFuture<String> combinedTask = asyncTask6.thenCombine(asyncTask7, (val1, val2) -> {
return val1 + " + " + val2;
});
- thenCombine: Combines two independent
CompletableFuture
s. - thenCombineAsync: Similar to
thenCombine
, but executes asynchronously.
Output:
Task 1 + Task 2
Both Future
and CompletableFuture
are essential for handling asynchronous tasks in Java. While Future
is simple and straightforward, CompletableFuture
offers greater flexibility, especially with task chaining, composition, and combining.