Understanding Runnable, Callable, and CompletableFuture in Java Concurrency
by Harvey Yao. Feb 28, 2024
Java, a versatile and widely-used programming language, provides several mechanisms for concurrent and asynchronous programming.
Java concurrency refers to the ability of the Java programming language to execute multiple tasks or threads concurrently. Concurrency in Java allows programs to make effective use of available system resources, improve performance, and enhance responsiveness. It enables the execution of multiple tasks simultaneously, either by running them in parallel or by interleaving their execution in a way that gives the illusion of parallelism.
Among Java Concurrency, the Runnable, Callable, and CompletableFuture interfaces play crucial roles in facilitating the execution of tasks in parallel, handling asynchronous operations, and managing concurrent computations. In this article, we will delve into each of these concepts, exploring their functionality, use cases, and how they contribute to building efficient and responsive Java applications.
Process and Thread Concept
Before diving into the intricacies of Runnable, Callable, and CompletableFuture in Java, it’s essential to establish a foundational understanding of the concepts of Processes and Threads. Processes and threads are fundamental concepts in the realm of concurrent programming, forming the backbone of how applications handle tasks concurrently and efficiently.
A process is an independent, self-contained unit of execution in a computing environment. It encapsulates the program code, data, and system resources required for its execution. Each process operates independently, with its memory space and resources, providing a level of isolation from other processes. Processes communicate with each other through inter-process communication (IPC) mechanisms.
Threads, on the other hand, are smaller units of execution within a process. A single process can have multiple threads, each running concurrently and sharing the same resources and memory space. Threads within a process enable the parallel execution of tasks, allowing for more responsive and efficient utilization of system resources. Threads share the process’s data, code, and open files, but they have their own execution stack.
Introduction to Runnable
The Runnable interface is a fundamental component of Java’s multithreading capabilities. It resides in the java.lang package and is designed for classes whose instances can be executed by a thread. The heart of the Runnable interface is its single method, run(). Any class that implements Runnable must provide an implementation for this method, specifying the code that should be executed in a separate thread.
Example of Runnable:
public class MyRunnableTask<T> implements Runnable{
private final MyCallable callable;
public MyRunnableTask(MyCallable callable) {
this.callable = callable;
}
@Override
public void run() {
try {
final T result = (T) callable.call();
callable.executeAfterFetching((String) result);
} catch (Exception e) {
throw new RuntimeException();
}
}
}
callable: An instance variable of type MyCallable
result: An instance variable of type T, representing the result that will be used when executing the task. Similarly, it is marked as final.
It’s important to note that the actual implementation of the MyCallable class and its executeAfterFetching method is provided in the next code snippet. The interaction between MyRunnableTask and MyCallable is to be designed for executing some task concurrently and handling the result.
This structure allows for a separation of concerns, where the runnable task (MyRunnableTask) is responsible for running the task, and the callable task (MyCallable) is responsible for defining what the task does and how it processes the result after fetching it.
Advantage of Runnable
The use of Runnable has several advantages, including simplicity and flexibility. By separating the task logic into a Runnable implementation, the code becomes more modular and easier to maintain. Additionally, it allows for better resource management as multiple runnables can be executed by a single thread or across multiple threads.
Introduction to Callable
While the Runnable interface is excellent for tasks that do not produce a result, the Callable interface extends its functionality by allowing tasks to return values and throw checked exceptions. The Callable interface resides in the java.util.concurrent package, making it part of the broader concurrency framework introduced in Java.
Example of Callable
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
private final String apiUrl;
private final String headerKey;
private final String headerValue;
public MyCallable(String apiUrl, String headerKey, String headerValue) {
this.apiUrl = apiUrl;
this.headerKey = headerKey;
this.headerValue = headerValue;
}
@Override
public String call() {
HttpURLConnection urlConnection;
try {
URL url = new URL(apiUrl);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty(headerKey, headerValue);
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
try (OutputStream os = urlConnection.getOutputStream()) {
Thread.sleep(1500);
byte[] input = "".getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
int responseCode = urlConnection.getResponseCode();
if (responseCode == 401) {
return "{\"rstatus\":\"002\"}";
}
try (BufferedReader br = new BufferedReader(
new InputStreamReader(urlConnection.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
return response.toString();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void executeAfterFetching(String result) {
System.out.println(result);
}
}
The class implements the Callable
The method performs an HTTP POST request to the specified API URL with the configured headers.
It includes a delay of 1500 milliseconds using Thread.sleep(1500) before sending an empty POST request.
The response is processed, and if the HTTP response code is 401, a predefined JSON string is returned.
Otherwise, the response from the server is read and returned as a string.
As for executeAfterFetching() method, It is intended to be called after the asynchronous task initiated by the call method is completed.
The MyCallable class is designed to perform an asynchronous HTTP POST request to a specified API URL, with configurable headers. The call method encapsulates this task, and the executeAfterFetching method is meant to handle the result after the asynchronous task is completed. The class leverages Java’s Callable interface for concurrent execution.
Main Class to Run the Entire Example
import org.example.runnable.MyCallable;
import org.example.runnable.MyRunnableTask;
public class Main {
public static void main(String[] args) {
String apiUrl = "http://localhost:8080/api/verify_session";
Thread myThread = new Thread(new MyRunnableTask<>(new MyCallable(apiUrl,"tk", "1aZw993VIAYHuDCONEnW8OpmA0") ));
myThread.start();
}
}
The main method serves as the entry point of the program. A string variable apiUrl is defined, representing the URL for an HTTP POST request. An instance of MyCallable is created, providing the API URL, header key (“tk”), and header value (“1aZw993VIAYHuDCONEnW8OpmA0”).
An instance of MyRunnableTask is created, and the MyCallable instance is passed as a parameter to its constructor.
A new thread (myThread) is created, and the MyRunnableTask instance is passed to its constructor.
The start method is called on the thread, initiating the asynchronous task defined in MyRunnableTask.
The executing flow is 1. The MyRunnableTask class, as explained in a previous response, is designed to run a task asynchronously, specifically an HTTP POST request to the provided API URL. 2. The MyCallable class is responsible for making the HTTP POST request and processing the result. 3. The combination of MyRunnableTask and MyCallable allows for the execution of a concurrent task in a separate thread, providing a level of parallelism.
The Main class leverages the Thread class to create a new thread, and within that thread, an instance of MyRunnableTask is executed, which, in turn, invokes the task defined in MyCallable. The overall design allows for parallel execution of tasks and is useful for scenarios where concurrent processing is required.
Introduction to CompletableFuture
Introduced in Java 8, the CompletableFuture class takes asynchronous programming in Java to a new level. It resides in the java.util.concurrent package and offers a versatile and powerful framework for managing asynchronous computations. CompletableFuture is not just a replacement for Runnable and Callable; it provides additional features for composing, combining, and handling asynchronous tasks. Detailed please check my article “Detailed Instructions for Exploring CompletableFuture”.
CompletableFuture offers several advantages over traditional approaches to asynchronous programming. One notable feature is the ability to chain multiple asynchronous operations, creating a pipeline of computations. Additionally, CompletableFuture provides methods for combining results from multiple asynchronous tasks, enabling more complex workflows.
Comparison and Use Cases
When deciding between Runnable and Callable, the key factor is whether the task needs to produce a result. If the computation is purely for side effects and doesn’t need to return a value, Runnable is sufficient. On the other hand, if the task produces a result that needs to be processed further, or if it may throw checked exceptions, Callable is the appropriate choice.
CompletableFuture provides a more modern and flexible approach to asynchronous programming compared to traditional approaches using Runnable or Callable along with Future. It excels in scenarios where chaining multiple asynchronous operations or combining results is required. However, for simpler tasks without complex dependencies, traditional approaches may be more straightforward.
Conclusion
In conclusion, understanding Runnable, Callable, and CompletableFuture is essential for developing efficient and responsive Java applications. The Runnable and Callable interfaces form the foundation of multithreading, allowing tasks to be executed concurrently. On the other hand, CompletableFuture introduces a more advanced and feature-rich framework for handling asynchronous operations, offering powerful tools for composition, combination, and error handling.
When choosing between Runnable and Callable, consider whether the task needs to produce a result or handle checked exceptions. For more complex asynchronous workflows, CompletableFuture provides a modern and expressive solution.
By mastering these concurrency mechanisms, Java developers can create applications that leverage the power of parallelism and asynchronous processing, ultimately leading to more responsive and scalable software solutions.
Enjoyed this post? If you found it valuable, consider supporting my work by leaving a tip. Your contributions help me continue creating content like this.
Buy me a coffee ☕️!