SpringBoot @Async: magic and pitfalls

Source: https://medium.com/

The @Async annotation is like the secret weapon for performance optimization in springboot projects. Yes, we can also manually create our own executors and thread pools, but @Async makes things simpler and more magical.

The @Async annotation allows us to run code in the background, so our main thread can continue running without waiting for slower tasks to complete. But, like any secret weapon, it’s important to use it wisely and understand its limitations.

Text

In this article, we’ll delve into the magic of@Async and what you should be aware of when using it in a Spring Boot project. First let’s learn the basics of how to use @Async in your application.

We need to enable @Async in Spring Boot application. To do this, we need to add the @EnableAsync annotation to the configuration class or main application file. This will enable asynchronous behavior for all methods in your application annotated with @Async.

@SpringBootApplication
@EnableAsync
public class BackendAsjApplication {
}

We also need to create a bean that specifies the configuration of the method annotated with @Async. We can set the maximum thread pool size, queue size, etc. However, be careful when adding these configurations. Otherwise, we might run out of memory very quickly. I also usually add a log to warn when the queue size is full and there are no more threads to receive new incoming tasks.

@Bean
 public ThreadPoolTaskExecutor taskExecutor() {
  ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  executor.setCorePoolSize(2);
  executor.setMaxPoolSize(2);
  executor.setQueueCapacity(500);
  executor.setThreadNamePrefix("MyAsyncThread-");
  executor.setRejectedExecutionHandler((r, executor1) -> log.warn("Task rejected, thread pool is full and queue is also full"));
  executor.initialize();
  return executor;
 }

Now, let’s use it. Let’s say we have a service class that contains methods we want to be asynchronous. We will annotate this method with @Async.

@Service
public class EmailService {
    @Async
    public void sendEmail() {
   
    }
}

In the code examples, you’ll see EmailService and PurchaseService mentioned multiple times. These are just examples. I don’t want to name everything “MyService”. So name it something more meaningful. In an e-commerce application, you would of course want your EmailService to be asynchronous so that customer requests are not blocked

Now, when we call this method, it will return immediately, freeing the calling thread (usually the main thread) to continue performing other tasks. The method will continue to execute in the background and return the results to the calling thread later. Since we marked the @Async method with void here, we’re not really interested in when it completes.

Pretty simple and pretty powerful, right? (Of course, we can do more configuration, but the above code is enough to run a fully asynchronous task)

However, before we start annotating all methods with @Async, there are a few things we need to be aware of.

@Async methods need to be in different classes

While using @Async annotation, it is important to note that we cannot call @Async method from the same class. This is because doing so will cause an infinite loop and cause the application to hang.

Here are examples of things not to do:

@Service
public class PurchaseService {

    public void purchase(){
        sendEmail();
    }

    @Async
    public void sendEmail(){
        // Asynchronous code
    }
}

Instead, we should use a separate class or service for asynchronous methods.

@Service
public class EmailService {

    @Async
    public void sendEmail(){
        // Asynchronous code
    }
}

@Service
public class PurchaseService {

    public void purchase(){
        emailService.sendEmail();
    }

    @Autowired
    private EmailService emailService;
}

Now you may be wondering, can I call an async method from another async method? The simplest answer is no. When an asynchronous method is called, it executes in a different thread, and the calling thread continues with the next task. If the calling thread itself is an asynchronous method, it cannot wait for the called asynchronous method to complete before continuing, which may result in unexpected behavior.

@Async and @Transcational don’t work well together

The @Transactional annotation is used to indicate that a method or class should participate in a transaction. It is used to ensure that a set of database operations are executed as a single unit of work and that the database remains in a consistent state in the event of any failure.

When a method is annotated with @Transactional, Spring creates a proxy around the method and all database operations within the method are performed within the transaction context. Spring is also responsible for starting the transaction before calling the method and committing the transaction after the method returns, or rolling it back if an exception occurs.

However, when you use the @Async annotation to make a method asynchronous, the method will execute in a separate thread from the main application thread. This means that the method is no longer executed within the context of a Spring-initiated transaction. Therefore, database operations within @Async methods do not participate in transactions, and the database may be in an inconsistent state when an exception occurs.

@Service
public class EmailService {

    @Transactional
    public void transactionalMethod() {
        //database operation 1
        asyncMethod();
        //database operation 2
    }

    @Async
    public void asyncMethod() {
        //database operation 3
    }
}

In this example, database operation 1 and database operation 2 are executed in the transaction context of Spring boot. However, database operation 3 is performed in a separate thread and is not part of the transaction.

So if an exception occurs before database operation 3 is executed, database operation 1 and database operation 2 will be rolled back as expected, but database operation 3 will not be rolled back. This may leave the database in an inconsistent state.

Of course, there are ways around this, namely using something like a TransactionTemplate to manage transactions, but out of the box, you’ll end up having problems if you call an async method from a conversion method.

@Async blocking problem

Let’s say this is the configuration of our @Async thread pool:

@Bean
 public ThreadPoolTaskExecutor taskExecutor() {
  ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  executor.setCorePoolSize(2);
  executor.setMaxPoolSize(2);
  executor.setQueueCapacity(500);
  executor.setThreadNamePrefix("MyAsyncThread-");
  executor.setRejectedExecutionHandler((r, executor1) -> log.warn("Task rejected, thread pool is full and queue is also full"));
  executor.initialize();
  return executor;
 }

This means that at any given moment, we will have at most 2 @Async tasks running. If more tasks come in, they will be queued until the queue size reaches 500.

But now suppose, one of our @Async tasks takes too much time to execute, or is simply blocked due to an external dependency. This means that all other tasks will be queued and not executed fast enough. Depending on your application type, this may cause delays.

One way to solve this problem is to use a separate thread pool for long-running tasks and a separate thread pool for tasks that are more urgent and do not require a lot of processing time. We can do this:

@Primary
 @Bean(name = "taskExecutorDefault")
 public ThreadPoolTaskExecutor taskExecutorDefault() {
  ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  executor.setCorePoolSize(2);
  executor.setMaxPoolSize(2);
  executor.setQueueCapacity(500);
  executor.setThreadNamePrefix("Async-1-");
  executor.initialize();
  return executor;
 }

 @Bean(name = "taskExecutorForHeavyTasks")
 public ThreadPoolTaskExecutor taskExecutorRegistration() {
  ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  executor.setCorePoolSize(2);
  executor.setMaxPoolSize(2);
  executor.setQueueCapacity(500);
  executor.setThreadNamePrefix("Async2-");
  executor.initialize();
  return executor;
 }

Then to use it, just add the name of the executor to the @Async declaration:

@Service
public class EmailService {
    @Async("taskExecutorForHeavyTasks")
    public void sendEmailHeavy() {
        //method implementation
    }
}

However, please note that we should not use @Async Object.wait() on methods that call Thread.sleep() or The purpose of @Async would be defeated.

Exceptions in @Async

afafab442235c2c9e8daa345d546a1bc.png

Another thing to remember is that @Async methods do not throw exceptions to the calling thread. This means you need to handle exceptions properly in @Async methods, otherwise they will be lost.

Here are examples of things not to do:

@Service
public class EmailService {

    @Async
    public void sendEmail() throws Exception{
        throw new Exception("Oops, cannot send email!");
    }
}

@Service
public class PurchaseService {
    
    @Autowired
    private EmailService emailService;

    public void purchase(){
        try{
            emailService.sendEmail();
        }catch (Exception e){
            System.out.println("Caught exception: " + e.getMessage());
        }
    }
}

In the above code, the exception is thrown in asyncMethod() but is not caught by the calling thread and the catch block is not executed.

To properly handle exceptions in @Async methods, we can use a combination of Future and try-catch blocks. Here is an example:

@Service
public class EmailService {

    @Async
    public Future<String> sendEmail() throws Exception{
        throw new Exception("Oops, cannot send email!");
    }
}

@Service
public class PurchaseService {

    @Autowired
    private EmailService emailService;

    public void purchase(){
        try{
            Future<String> future = emailService.sendEmail();
            String result = future.get();
            System.out.println("Result: " + result);
        }catch (Exception e){
            System.out.println("Caught exception: " + e.getMessage());
        }
    }
}

By returning a Future object and using a try-catch block, we can properly handle and catch exceptions thrown in @Async methods.

In summary, the @Async annotation in Spring Boot is a powerful tool for improving application performance and scalability. However, it’s important to use it with care and be aware of its limitations. By understanding these pitfalls and using technologies like CompletableFuture and Executor, you can take full advantage of the @Async annotation and take your application to the next level.

cffcdf74259dfdf87f512487c7853015.png

Recommended in the past

Engineers who switched jobs from Alibaba write try catches in such an elegant way!

Microservices Framework Battle: Is Quarkus a SpringBoot Replacement?

The perfect combination of Redis and Spring Boot: the black technology of Lua scripting

Use Redis to query “people nearby”

ae64ece2fc6b07dc40925956026348f7.gif

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Java Skill TreeHomepageOverview 139332 people are learning the system