Spring is a very powerful framework for building Java applications. It hides most of the complexities, such as managing database transactions with @Transactional, running asynchronous code with @Async, consuming Kafka messages with just @KafkaListener, and so on, which makes the life of a developer easy.
In this blog, I’ll be going through @Async annotation and how we can make use of it to run any code block asynchronously. Just by adding @Async to the method that we want to run asynchronously, that method will be executed in a separate thread.
Sample Code
@RestController
public class AuthenticationController {
@Autowired
private MailService mailService;
@PostMapping("/sing-up")
public ResponseEntity<Boolean> singUp(@RequestBody UserDetails userDetails) {
boolean isSuccess = authenticationService.createuser(userDetails);
if (isSuccess) {
mailService.sendVerificationEmail(userDetails.getEmail());
}
return new ResponseEntity<>(isSuccess, HttpStatus.OK);
}
}
—-----------------------------------------------------------------------
@Service
public class MailService {
@Async
public void sendVerificationEmail(String email) {
System.out.println("Sending verification email to email: " + email);
}
}
@SpringBootApplication
@EnableAsync
public class ThreadsApplication {
public static void main(String[] args) {
SpringApplication.run(ThreadsApplication.class, args);
}
—-----------------------------------------------------------------------
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setThreadNamePrefix("Async-");
threadPoolTaskExecutor.setCorePoolSize(2);
threadPoolTaskExecutor.setMaxPoolSize(6);
threadPoolTaskExecutor.setQueueCapacity(5);
return threadPoolTaskExecutor;
}
Here for now let’s concentrate only on the first 2 code blocks, will come to the 3rd code block later in the same blog.
When a request is received for creating a user, we call the createUser() method of some class to register the user, and it returns the registration status. If registration is successful, we want to send a verification email to the user; this is where we can make use of @Asyncannotation. MailService can be slow due to various reasons and it is better to return to frontend as quickly as possible so that end users don’t feel slowness.
Let’s start discussing the mistakes that we may make here.
Mistake 1: Not following rules of @Async
In order to make @Async annotation work properly we have to follow 2 major rules
- Method annotated with @Async should be public
- Method annotated with @Async should be called from different class, i.e calling async method within the same class doesn’t work
For more details refer Aspect Oriented Programming with Spring
Now, let’s have a look at what’s going on in our third code block — this is where the magic and confusion begin.
In order for @Async to work, we need to add @EnableAsyncannotation to any of the configuration classes; here, we have added it to our main class annotated with @SpringBootApplication.
Along with this, spring also expects us to provide bean which defines thread pool-related details. According to spring documentation
“By default, Spring will be searching for an associated thread pool definition: either a unique TaskExecutor bean in the context, or an Executor bean named "taskExecutor" otherwise. If neither of the two is resolvable, a SimpleAsyncTaskExecutor will be used to process async method invocations.”
Great, we have also provided the bean taskExecutor with thread pool-related details specified, Now you might be thinking, "What is the magic or confusion here?" Let’s take a look at the taskExecutor bean we have defined.
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setThreadNamePrefix("Async-");
threadPoolTaskExecutor.setCorePoolSize(2);
threadPoolTaskExecutor.setMaxPoolSize(6);
threadPoolTaskExecutor.setQueueCapacity(5);
return threadPoolTaskExecutor;
}
In the first line, we are creating an instance of ThreadPoolTaskExecutor, which is a wrapper provided by Spring on top of ThreadPoolExecutor. The second line doesn’t need any explanation.
The third, fourth, and fifth lines are more important because they lead to our Mistake 2.
Mistake 2: Not understanding how ThreadPoolExecutor is implemented
Assumption that we make looking into the above code: Spring will create new threads for each task until MaxPoolSize is reached, and for upcoming tasks if the thread count is greater than or equal to MaxPoolSize, the tasks are queued up to QueueCapacity, and any other further tasks are ignored after the QueueCapacity and MaxPoolSize limits are reached.
ThreadPoolExecutor’s way of implementation: New threads are created for each task until CorePoolSize is reached, after which the tasks are queued up to QueueCapacity, and once QueueCapacity is reached, new threads are created up to MaxPoolSize, and when all three parameters reach their limits, tasks will be rejected.
I know you have not understood anything. To make it more clear, I’ve planned to explain the above concept in two ways.
- Using code
- Using Animation — My favorite way
Understanding through Code
@RestController
public class ThreadController {
@Autowired
private ThreadService threadService;
@GetMapping("/async/{number}")
public void async(@PathVariable long number) {
try {
threadService.printNumber(number);
} catch (RejectedExecutionException e) {
System.out.println(
"Rejecting task since queue is full and no threads are free for task number: " + number);
}
}
@PostConstruct
public void init() throws InterruptedException {
for (int i=1; i<=11; i++) {
threadService.printNumber(i);
Thread.sleep(100);
}
}
}
—-------------------------------------------------------------------
@Service
public class ThreadService {
@Async
public void printNumber(long num) {
System.out.println(num);
try {
Thread.sleep(60000);
}
catch (InterruptedException e) {
System.out.println("Error while executing sleep in Thread for task: " + num);
}
}
}
Here I’m triggering the @Async method printNumber inside the init() method annotated with @PostConstruct; when the app starts, this init() method will be called. The code just loops over 11 iterations and calls the printNumber() method, and inside that method I’ve added a sleep of 1 minute. When app starts and when we see below log I’ll hit API http://localhost:8082/async/100 to illustrate how tasks will be rejected when all three parameters (CorePoolSize, MaxPooSize and QueueCapacity) have reached their limits.
2023-02-26 15:21:48.959 INFO 64720 --- [ main] com.async.async.AsyncApplication : Started AsyncApplication in 0.914 seconds (JVM running for 1.126)
Output — When we follow above steps we get output in below order
1
2
8
9
10
11
2023-02-26 15:26:10.734 INFO 64785 --- [ main] com.async.async.AsyncApplication : Started AsyncApplication in 2.035 seconds (JVM running for 2.254)
Rejecting task since queue is full and no threads are free for task number: 100
3
4
5
6
7
As we can see, first tasks 1 and 2 are executed, which are because of the CorePoolSize — 2, and after that, tasks 3, 4, 5, 6, & 7 are queued and QueueCapacity of 5 is reached, after that, new tasks 8, 9, 10 & 11 are assigned new threads up to MaxPoolSize — 6 here, though MaxPoolSize is 6 only 4 new threads can be created because 2 threads are taken initially by task-1 and task-2 and they are still not completed (remember we added sleep of 1min, that’s why it is not completed).
Now all three parameters have reached the limit, and when I hit the API at http://localhost:8082/async/100, a RejectedExecutionException is raised. We are handling that exception and printing something, which is why we are seeing a log Rejecting task since queue is full and no threads are free for task number: 100 after 1, 2, 8, 9, 10, 11 are printed.
Finally, after one minute, threads will be free, tasks are dequeued from the queue, and each is assigned a thread, because of which we can see that 3, 4, 5, 6, 7 are printed.
Understanding through Animation — This is my favorite part, instead of reading 100’s of lines spending 50s is enough to understand.
So be careful if you are expecting tasks to be executed in the order, you may have to play around with QueueCapacityparameter
This is my first blog, and I might have made lots of mistakes. Please let me know if there are any mistakes in the comments.