Implementing Asynchronous Processing in Spring Boot with the @Async Annotation
Spring Boot is a robust framework that simplifies the development of Java applications. One of its powerful features is the support for asynchronous method execution. By using the @Async annotation, developers can execute methods asynchronously, allowing for non-blocking operations and enhancing the performance and responsiveness of applications. This article explores the @Async annotation in Spring Boot, demonstrating how to implement and use it effectively.
Understanding Asynchronous Programming
Asynchronous programming enables a method to run in the background without blocking the main thread. This approach is particularly useful in scenarios where a task might take a long time to complete, such as:
Making external API calls
Performing I/O operations (e.g., reading/writing files)
Running complex calculations
With asynchronous calls, the application can continue executing other tasks while the long-running process completes, thus improving the overall efficiency and responsiveness.
Setting Up Spring Boot for Asynchronous Execution
Step 1: Enable Async Support
To use the @Async annotation, you need to enable asynchronous processing in your Spring Boot application. This is done by adding the @EnableAsync annotation to a configuration class:
Step 2: Annotate Methods with @Async
Once asynchronous processing is enabled, you can use the @Async annotation on methods that you want to run asynchronously. Here’s an example of a service class with an asynchronous method:
In this example, the performAsyncTask method is annotated with @Async, indicating that it should be executed asynchronously.
Step 3: Call the Asynchronous Method
To call the asynchronous method, simply inject the service and invoke the method as usual. Spring Boot will handle the asynchronous execution:
When the /trigger-async endpoint is hit, the performAsyncTask method runs asynchronously, allowing the HTTP request to return immediately while the task continues in the background.
Handling Return Values with CompletableFuture
In some cases, you may need to handle the result of an asynchronous operation. This can be achieved using CompletableFuture. add the service method to return a CompletableFuture:
Update the controller to handle the CompletableFuture:
Now, the /trigger-async-future endpoint returns a CompletableFuture that will be completed once the asynchronous task is finished.
Configuring Async Executor
By default, Spring Boot uses a simple executor for asynchronous processing. You can customize the executor by defining a ThreadPoolTaskExecutor bean:
This configuration defines a thread pool with specific parameters such as core pool size, max pool size, and queue capacity.
@Bean(name = "taskExecutor"): This annotation tells Spring that this method returns a bean to be managed by the Spring container. The name taskExecutor is assigned to this bean.
ThreadPoolTaskExecutor Configuration
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();: Creates a new instance of ThreadPoolTaskExecutor.
executor.setCorePoolSize(2);: Sets the core number of threads. This is the number of threads that will be kept in the pool, even if they are idle. In this example, at least 2 threads will be maintained.
executor.setMaxPoolSize(5);: Sets the maximum number of threads in the pool. When there are more than 5 concurrent tasks, additional tasks will wait in the queue until a thread becomes available.
executor.setQueueCapacity(500);: Sets the capacity of the queue that holds tasks before they are executed. If all core threads are busy and the queue is not full, new tasks will wait in this queue. Here, up to 500 tasks can be queued.
executor.setThreadNamePrefix("Async-");: Sets the prefix for the names of threads created by this executor. This can help with debugging by providing a way to identify threads created for asynchronous execution.
executor.initialize();: Initializes the executor with the specified configuration. This method must be called before the executor is used.
Using the Custom TaskExecutor
By defining this custom TaskExecutor bean, Spring Boot will use it for any method annotated with @Async. Here’s how the asynchronous processing will work:
When a method annotated with @Async is called, Spring looks for a bean of type TaskExecutor.
If a TaskExecutor bean named taskExecutor is found, it will be used to run the method in a separate thread.
The ThreadPoolTaskExecutor ensures that the asynchronous tasks are executed according to the configured thread pool settings.
Benefits of Custom TaskExecutor
Using a custom TaskExecutor offers several benefits:
Controlled Concurrency: You can manage the number of concurrent threads, preventing resource exhaustion.
Queue Management: By setting the queue capacity, you can handle a large number of tasks without overwhelming the system.
Thread Naming: Custom thread names can simplify debugging and monitoring.
Scalability: Proper configuration allows your application to handle varying loads efficiently.
By tuning these parameters based on your application’s requirements, you can optimize the performance and resource utilization of your Spring Boot application.
Calculating the ideal number for corePoolSize, maxPoolSize, and queueCapacity in a ThreadPoolTaskExecutor involves understanding the workload characteristics of your application, including task duration, task arrival rate, and system resources. Here are steps and considerations for calculating these values:
1. Understand Your Workload
Task Duration: How long does each task typically take to execute? Measure or estimate the average and peak durations.
Task Arrival Rate: How frequently do new tasks arrive? Measure the average and peak rates at which tasks are submitted.
Task Nature: Are the tasks CPU-bound, I/O-bound, or a mix of both? CPU-bound tasks benefit more from parallel execution than I/O-bound tasks.
2. System Resources
CPU Cores: How many CPU cores are available? For CPU-bound tasks, you don't want to exceed the number of cores significantly.
Memory: How much memory does each task consume? Ensure that increasing the queue capacity and thread pool size does not exhaust system memory.
3. Calculating Core Pool Size (corePoolSize)
For CPU-bound tasks:
A good starting point is to set corePoolSize to the number of available CPU cores. This ensures that each task has its own core and minimizes context switching.
Formula: corePoolSize = Number of CPU cores
For I/O-bound or mixed tasks:
Since I/O-bound tasks spend time waiting, you can have more threads than CPU cores.
Estimate the ratio of waiting time to processing time (WT/PT).
Formula: corePoolSize = Number of CPU cores * (1 + WT/PT)
4. Calculating Maximum Pool Size (maxPoolSize)
maxPoolSize should accommodate peak loads. This can be higher than corePoolSize, but going too high may lead to resource exhaustion.
If you expect occasional bursts of tasks, set maxPoolSize to a higher value to handle spikes.
Formula: maxPoolSize = corePoolSize * 2 (as a starting point, adjust based on monitoring)
5. Calculating Queue Capacity (queueCapacity)
Queue capacity determines how many tasks can wait when all core threads are busy.
High queue capacity allows for handling large bursts of tasks without rejection but increases memory usage.
Low queue capacity may lead to task rejections under heavy load but keeps memory usage lower.
Considerations:
If tasks are short and arrive quickly, a larger queue might be necessary.
If tasks are long-running, a smaller queue may be sufficient.
Example Calculation
Suppose you have a system with 8 CPU cores, and tasks are a mix of CPU-bound and I/O-bound with a waiting time to processing time ratio of 3:1 (WT/PT = 3).
Core Pool Size Calculation:
corePoolSize = Number of CPU cores * (1 + WT/PT)
corePoolSize = 8 * (1 + 3)
corePoolSize = 8 * 4 = 32
Max Pool Size Calculation:
A starting point might be double the core pool size, depending on your system's capacity.
maxPoolSize = corePoolSize * 2
maxPoolSize = 32 * 2 = 64
Queue Capacity Calculation:
If tasks are expected to arrive in bursts and you want to handle up to 5000 waiting tasks:
Ensure the system has enough memory to handle this queue size.
Monitoring and Tuning
After deploying your application, monitor the following metrics:
Thread Pool Usage: Ensure threads are not idle when there are tasks to be processed.
Queue Length: Ensure tasks are not spending too much time in the queue.
System Resources: Monitor CPU and memory usage to avoid resource exhaustion.
Task Execution Time: Ensure tasks are completing in a reasonable time.
Based on these metrics, adjust corePoolSize, maxPoolSize, and queueCapacity to optimize performance and resource utilization.
By carefully analyzing and adjusting these parameters, you can find the ideal configuration that balances performance, resource usage, and responsiveness for your specific workload and system environment.
final code for reference:
https://github.com/prabhatpankaj/asynchronous-call-springboot/
Great insights Prabhat Pankaj! Our editorial team decided to feature you among the top Spring Boot developers: https://echoglobal.tech/technologies/spring-boot/
Java | Angular 17 | Full Stack Developer | Spring/Springboot | Microservices | MEAN | AWS(EKS, EC2, ALB, RDS) | CI/CD(Jenkins, Docker, Kubernetes, Sonar) | NodeJS | Trainer | Technical Specialist| Freelancer | Part-time
6moThanks for sharing.