Asynchronous Programming: Java's CompletableFuture
Kevin Muchene
In everyday life, we often perform tasks asynchronously; for example, toasting bread while the coffee is brewing. Modern applications assume a similar strategy to improve performance and responsiveness by leveraging asynchronous programming.
JavaScript Promises vs. Java's CompletableFuture
In JavaScript, we typically use Promises
to handle asynchronous operations.
A comparable feature in Java is CompletableFuture
, which Venkat Subramaniam affectionately refers to as "the Promises of Java" in his talk.
In this article, we'll briefly compare JavaScript Promises
and Java's CompletableFuture
.
We'll then focus on applying asynchronous programming in Spring Boot using the @Async
annotation and CompletableFuture
to improve performance and responsiveness.
JavaScript
const fetchData = () => {
return new Promise((resolve, reject) => {
// After 1 second, the promise is resolved with the value "Data fetched".
setTimeout(() => resolve("Data fetched"), 1000);
});
};
// Call the fetchData function, which returns a promise
fetchData()
// Handle the resolved value of the promise
.then(console.log)
// Handle any errors that occur during the promise execution
.catch(console.error);
.then() is used to specify the actions after the asynchronous operation completes successfully and .catch() is used to handle any errors.
Java
private static CompletableFuture<String> startAsyncTask(String result) {
// Create and return a CompletableFuture that executes the given task asynchronously
return CompletableFuture.supplyAsync(() -> result);
}
public static void main(String[] args) {
// Call the asynchronous task and handle the result once it completes
startAsyncTask("Data fetched")
// Define an action to take when the task completes successfully
.thenAccept(System.out::println);
}
.thenApply() in Java CompletableFuture
is similar to .then() in JavaScript;
it transforms the result of a completed future.
.thenAccept() is used to consume the result of a future without transforming
it.
The above examples illustrate how asynchronous tasks in both JavaScript and Java return an object (Promise or CompletableFuture)
that can be chained with callbacks to handle success or error outcomes.
Next, we are going to look at asynchronous programming in Spring Boot using the @Async annotation and CompletableFutures
.
Asynchronous Programming in Spring Boot
When dealing with time-consuming tasks (e.g., calling external APIs, processing large datasets, etc), blocking the main thread can degrade application performance.
To solve this, Spring Boot offers the @Async
annotation, which makes it easy to execute tasks in separate threads with minimal manual configuration.
Key Benefits of Using @Async in Spring Boot
- Non-blocking Execution: Long-running tasks can run independently of the main thread, improving responsiveness.
- Simple Configuration: With minimal setup, you can enable asynchronous behavior in your application.
- Seamless Integration:
@Async
works natively with Spring components, allowing straightforward integration into your existing code.
Example: Synchronous vs. Asynchronous Method
We have a fetchAllUsers
method that returns a list of users.
Synchronous fetchAllUsers() Method
@Override
public List<UserDTO> fetchAllUsers() {
try {
// Synchronously fetches all users from the database
return convertUserDTOList(userRepository.findAll());
} catch (Exception ex) {
// Logs the error if an exception occurs
log.error("Failed to fetch users from the database", ex);
// Returns an empty list as a fallback
return Collections.emptyList();
}
}
This method runs synchronously, meaning the calling thread waits for the database operation and processing to complete before continuing execution. No separate threads are used; everything happens in the calling thread.
Disadvantage: This method blocks the calling thread, which could degrade performance and responsiveness in applications with many concurrent requests.
Asynchronous fetchAllUsers() Method
// Marks the method to be executed asynchronously
@Async
@Override
public CompletableFuture<List<UserDTO>> fetchAllUsers() {
// Asynchronously fetches all users and converts them to a list of UserDTO
return CompletableFuture.supplyAsync(
() -> convertUserDTOList(userRepository.findAll())
).handle((result, ex) -> {
if (ex != null) {
// Log the error and return an empty list
log.error("Failed to fetch users from the database", ex);
return Collections.emptyList();
}
return result;
});
}
Customizing the Thread Pool
If you want more control over the thread pool (for instance, to tune core and max threads), you can define a custom executor in a Spring configuration class:
// Marks this class as a Spring configuration class
@Configuration
public class AsyncBean {
// Defines a bean to be managed by the Spring container
@Bean()
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// Sets the core pool size, the minimum number of threads in the pool
executor.setCorePoolSize(10);
// Sets the maximum number of threads allowed in the pool
executor.setMaxPoolSize(15);
// Specifies the capacity of the task queue before tasks are rejected or additional threads are created
executor.setQueueCapacity(50);
// Sets a custom prefix for thread names to help identify threads in logs or debugging
executor.setThreadNamePrefix("CustomThread-");
// Initializes the thread pool executor
executor.initialize();
// Returns the configured executor
return executor;
}
}
This code defines a Spring configuration class that sets up a custom thread pool for asynchronous execution in a Spring Boot application. The custom thread pool is configured using a ThreadPoolTaskExecutor
bean.
This bean is used by Spring's asynchronous processing (@Async
) to execute tasks in a controlled thread pool instead of relying on the default executor.
If you define a bean like this, Spring will automatically use it for all methods annotated with @Async
NB: If you decide to use more than one Executors you can define the bean with a name; for example,
@Bean(name = “taskExecutor”)
And in your method you specify the bean executor you want to use:
@Async(name=”taskExecutor”)
public CompletableFuture<List<UserDTO>> fetchAllUsers() {}
Advantages and Disadvantages of using thread pool management in Spring
Boot for @Async
Tasks and CompletableFuture
:
Advantages
- Improved Performance: Thread pools reduce the overhead of creating and destroying threads repeatedly. The system handles high workloads efficiently by reusing threads, improving application responsiveness.
- Scalability: Thread pool configuration allows fine-tuning for specific workloads, enabling better scalability for concurrent tasks.
- Non-blocking Asynchronous Processing: The combination of
@Async
andCompletableFuture
enables non-blocking operations, which is crucial for high-throughput applications like APIs or microservices. - Task Isolation: Thread pools isolate asynchronous tasks from the main application thread, preventing the blocking of critical processes.
- Error Handling Support: With
CompletableFuture
, you can gracefully handle exceptions(.exceptionally() or .handle())
, ensuring robust error recovery even in complex asynchronous flows.
Disadvantages
- Thread Contention: Misconfigured thread pools can lead to contention for resources, reducing application performance.
- Risk of Thread Starvation: If tasks in the thread pool are long-running or blocking, other tasks might be delayed or starved, impacting responsiveness.
- Increased Complexity: Thread pool management introduces additional layers of complexity, such as monitoring, debugging, and configuring thread behavior.
- Resource Consumption: Threads consume system resources like memory and CPU. A poorly configured thread pool can lead to high memory usage or CPU exhaustion.
- Deadlocks: Improper task dependencies or nested asynchronous calls can lead to deadlocks if threads are waiting on each other.
Tip
Always apply timeouts
to your asynchronous tasks to prevent long-running operations from consuming threads indefinitely.
This ensures that your application remains responsive even under adverse conditions.
Conclusion
Asynchronous programming is essential for building responsive and scalable applications, whether you're working with JavaScript or Java.
In Java, the CompletableFuture API
provides a powerful mechanism for managing asynchronous tasks, comparable to JavaScript Promises.
By leveraging Spring Boot's @Async
annotation and optionally configuring custom thread pools, you can prevent blocking your main application thread and handle high concurrency more efficiently.
When used wisely, asynchronous programming enhances performance, user satisfaction, and overall application robustness; making it a vital tool in modern software development.
What do you think about this approach?