Using @Async
in Spring Boot: A performance booster
In this blog, we dive into the power of Spring Boot’s @Async
annotation, showing you how to supercharge your application by running tasks concurrently. Learn how to reduce execution time, boost efficiency, and take full advantage of multi-threading with simple, practical examples. Perfect for developers looking to optimize their Spring Boot applications!
We will explore the use of @Async
annotation in two common scenarios:
- Making child methods asynchronous to improve the performance of the parent method.
- Running a method asynchronously in batches using multi-threading.
Let’s dive into the details and see how you can leverage @Async
to optimize your Spring Boot applications.
Make your app ready to boost
First we have to enable the power of @Async, we will do this by creating a class with some annotations — @Configuration and @EnableAsync
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, obj) -> {
System.err.println("Exception in async method: " + method.getName() + " - " + throwable.getMessage());
};
}
@Bean(name = "taskExecutor")
public AsyncTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // Adjust these values based on expected load
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsyncExecutor-");
executor.initialize();
return executor;
}
}
Create this class in some package that your app can scan.
Tip: Best is to create a package called
configuration
to keep all the configurations (classes annotated with @Configuration).
Explanations:
The AsyncConfigurer
interface allows us to configure the task executor that will be used to handle the asynchronous tasks, for example how many concurrent tasks it can run etc. and create a custom exception handler for exceptions that occurred in async method.
Always adjust the thread core pool size and max size according to the memory and cpu capacity of the system on which app is going to run.
1. Using @Async
to Make Child Methods Asynchronous
Scenario: Parent Method Calling Multiple Child Methods
In many cases, a method may need to call multiple child methods to complete its task. If these child methods are independent of each other, running them synchronously (one after the other) can unnecessarily delay the overall execution. By using @Async
on the child methods, we can make them run in parallel, reducing the overall time for the parent method to complete.
Example: Parent Method Calling Multiple Independent Methods
Imagine we have a processOrder()
method that needs to perform multiple tasks (like validating an order, calculating the shipping cost, and checking inventory). These tasks are independent, and by running them asynchronously, we can save time.
1.1 Create the Service with Asynchronous Methods
Let’s create a service where we have a parent method that calls multiple child methods, which we will make asynchronous.
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class OrderService {
// Parent method that calls asynchronous child methods
public void processOrder() {
System.out.println("Processing order...");
// Call child methods asynchronously
CompletableFuture<Void> validationTask = validateOrder();
CompletableFuture<Void> shippingTask = calculateShippingCost();
CompletableFuture<Void> inventoryTask = checkInventory();
// Wait for all tasks to finish
CompletableFuture.allOf(validationTask, shippingTask, inventoryTask).join();
System.out.println("Order processed successfully!");
}
@Async
public CompletableFuture<Void> validateOrder() {
System.out.println("Validating order...");
try {
Thread.sleep(2000); // Simulate time-consuming task
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Order validated.");
return CompletableFuture.completedFuture(null);
}
@Async
public CompletableFuture<Void> calculateShippingCost() {
System.out.println("Calculating shipping cost...");
try {
Thread.sleep(3000); // Simulate time-consuming task
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Shipping cost calculated.");
return CompletableFuture.completedFuture(null);
}
@Async
public CompletableFuture<Void> checkInventory() {
System.out.println("Checking inventory...");
try {
Thread.sleep(1000); // Simulate time-consuming task
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Inventory checked.");
return CompletableFuture.completedFuture(null);
}
}
1.2 Run the Parent Method
Finally, you can call the processOrder()
method from a controller or another service to test its functionality.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/process-order")
public String processOrder() {
orderService.processOrder();
return "Order is being processed!";
}
}
Expected Output
When you call the /process-order
endpoint, you will notice that the child methods run concurrently, reducing the overall processing time.
Processing order...
Validating order...
Calculating shipping cost...
Checking inventory...
Inventory checked.
Order validated.
Shipping cost calculated.
Order processed successfully!
In this example, while the validateOrder()
and calculateShippingCost()
methods take time to execute, they run in parallel, significantly reducing the time for the processOrder()
method to complete.
2. Running Methods in Batches Using @Async
for Multi-Threading
Scenario: Running a Method in Parallel to Process Large Data
In some cases, you may want to execute a method asynchronously in multiple threads to process large datasets or tasks in parallel, speeding up their execution. For example, if you need to process a large number of records from a database or handle batch tasks, you can divide the workload into smaller batches and execute each batch in its own thread using @Async
.
Example: Processing Data in Parallel
Let’s assume you have a task that needs to process a list of items. Instead of processing them sequentially, we can use @Async
to process them in parallel in smaller batches.
2.1. Create a Batch Processing Service
In this example, we’ll create a service to process a large list of numbers in batches.
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Service
public class BatchProcessingService {
public void processBatch(List<Integer> numbers) {
int batchSize = 5;
int totalBatches = (int) Math.ceil((double) numbers.size() / batchSize);
// Divide the list into smaller batches and process them asynchronously
for (int i = 0; i < totalBatches; i++) {
int fromIndex = i * batchSize;
int toIndex = Math.min((i + 1) * batchSize, numbers.size());
List<Integer> batch = numbers.subList(fromIndex, toIndex);
processBatchAsync(batch);
}
}
@Async
public CompletableFuture<Void> processBatchAsync(List<Integer> batch) {
System.out.println("Processing batch: " + batch);
try {
Thread.sleep(1000); // Simulate some processing time
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Processed batch: " + batch);
return CompletableFuture.completedFuture(null);
}
}
2.2 Create a Controller to Trigger Batch Processing
You can now create a controller to trigger the batch processing.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
public class BatchProcessingController {
@Autowired
private BatchProcessingService batchProcessingService;
@GetMapping("/process-batch")
public String processBatch() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
batchProcessingService.processBatch(numbers);
return "Batch processing started!";
}
}
Expected Output
When you call the /process-batch
endpoint, the numbers will be processed in batches concurrently, reducing the overall processing time.
Processing batch: [1, 2, 3, 4, 5]
Processing batch: [6, 7, 8, 9, 10]
Processing batch: [11, 12, 13, 14, 15]
Processed batch: [1, 2, 3, 4, 5]
Processed batch: [6, 7, 8, 9, 10]
Processed batch: [11, 12, 13, 14, 15]
Explanation
- We divided the list of numbers into smaller batches and processed each batch asynchronously using
@Async
. - This approach allows parallel processing of multiple batches, significantly reducing the total processing time compared to a sequential approach.
Conclusion
The @Async
annotation in Spring Boot is a powerful tool for improving the performance of your applications by allowing tasks to run concurrently. In this blog, we covered two common use cases:
- Making child methods asynchronous: When a parent method calls multiple independent child methods, marking the child methods as
@Async
allows them to run in parallel, reducing the overall execution time. - Batch processing with multi-threading: When you need to process large datasets or tasks in parallel, dividing the workload into smaller batches and processing each batch asynchronously can significantly improve performance.
By leveraging the power of asynchronous processing, you can optimize your applications, particularly in scenarios with heavy I/O-bound or time-consuming operations.