DEV Community

Ilkin
Ilkin

Posted on

Understanding @Async in Spring Boot

Asynchronous approach

With the high development of hardware & software, modern applications become much more complex and demanding. Due to
high demand engineers always try to find new ways to improve their application performance and responsiveness. One solution to slow-paced applications is the implementation of the Asynchronous approach. Asynchronous processing is a technique that is a process or function that executes a task to run concurrently, without waiting for one task to complete it before starting another. In this article, I will try to explore the Asynchronous approach and @Async annotation in Spring Boot, trying to explain the differences between multi-threading and concurrency, and when to use or avoid it.

Table of contents

What is @Async in Spring?

The @Async annotation in Spring enables asynchronous processing of a method call. It instructs the framework to execute the method in a separate thread, allowing the caller to proceed without waiting for the method to complete. This
improves the overall responsiveness and throughput of an application.

To use @Async, you must first enable asynchronous processing in your application by adding the @EnableAsync annotation
to a configuration class:

@Configuration
@EnableAsync
public class AppConfig {
}
Enter fullscreen mode Exit fullscreen mode

Next, annotate the method you want to execute asynchronously with the @Async annotation:


@Service
public class AsyncService {
    @Async
    public void asyncMethod() {
        // Perform time-consuming task
    }
}

Enter fullscreen mode Exit fullscreen mode

How is @Async different from multithreading and Concurrency?

Sometimes It might seem confusing to differentiate multithreading and concurrency from parallel execution, however, both are related to parallel execution. Each of them has their use case and implementation:

  • @Async annotation is Spring Framework-specific abstraction, which enables asynchronous execution. It gives the ability to use async with ease, handling all hard work in the background, such as thread creation, management, and execution. This allows users to focus on business logic rather than low-level details.

  • Multithreading is a general concept, commonly referring to the ability of an OS or program to manage multiple threads concurrently. As @Async helps us to do all hard work automatically, in this case, we can handle all this work manually and create a multithreading environment. Java has necessary classes such as Thread and ExecutorService to create and work with multithreading.

  • Concurrency is a much broader concept, and it covers both multithreading and parallel execution techniques. It is the
    ability of a system to execute multiple tasks simultaneously, on one or more processors across.

In summary, @Async is a higher-level abstraction that simplifies asynchronous processing for developers, on the other hand, multithreading and concurrency are more about to manual management of parallel execution.

When to use @Async and when to avoid it.

It seems very intuitive to use an asynchronous approach, however, it must be taken into account, there's do's and don'ts for this approach as well.

Use @Async when:

  • You have independent, time-consuming tasks that can run concurrently without affecting the application's responsiveness.
  • You want a simple and clean way to enable asynchronous processing without diving into low-level thread management.

Avoid using @Async when:

  • The tasks you want to execute asynchronously have complex dependencies or need a lot of coordinating. In such cases, you might need to use more advanced concurrency APIs, like CompletableFuture, or reactive programming libraries like Project Reactor.
  • You must have precise control over how threads are managed., such as custom thread pools or advanced synchronization mechanisms. In these cases, consider using Java's ExecutorService or other concurrency utilities.

Using @Async in a Spring Boot application.

In this example, we will create a simple Spring Boot application that demonstrates the use of @Async.
Let's create a simple Order management service.

  1. Create a new Spring Boot project with minimum dependency requirements:

    org.springframework.boot:spring-boot-starter
    org.springframework.boot:spring-boot-starter-web
    Web dependency is for REST endpoint demonstration purpose. @Async comes with boot starter.

  2. Add the @EnableAsync annotation to the main class or Application Config class, if we are using it.:

@SpringBootApplication
@EnableAsync
public class AsyncDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(AsyncDemoApplication.class, args);
  }
}
Enter fullscreen mode Exit fullscreen mode
@Configuration
@EnableAsync
public class ApplicationConfig {}
Enter fullscreen mode Exit fullscreen mode
  1. For the optimal solution, what we can do is, create a custom Executor bean and customize it as per our needs in the same Configuration class:
   @Configuration
   @EnableAsync
   public class ApplicationConfig {

     @Bean
     public Executor getAsyncExecutor() {
       ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
       executor.setCorePoolSize(5);
       executor.setMaxPoolSize(10);
       executor.setQueueCapacity(100);
       executor.setThreadNamePrefix("AsyncThread-");
       executor.initialize();
       return executor;
       }
   }
Enter fullscreen mode Exit fullscreen mode

With this configuration, we have control over max and default thread pool size. As well as other useful customizations.

  1. Create an OrderService class with @Async methods:
@Service
public class OrderService {

  @Async
  public void saveOrderDetails(Order order) throws InterruptedException {
    Thread.sleep(2000);
    System.out.println(order.name());
  }

  @Async
  public CompletableFuture<String> saveOrderDetailsFuture(Order order) throws InterruptedException {
    System.out.println("Execute method with return type + " + Thread.currentThread().getName());
    String result = "Hello From CompletableFuture. Order: ".concat(order.name());
    Thread.sleep(5000);
    return CompletableFuture.completedFuture(result);
  }

  @Async
  public CompletableFuture<String> compute(Order order) throws InterruptedException {
    String result = "Hello From CompletableFuture CHAIN. Order: ".concat(order.name());
    Thread.sleep(5000);
    return CompletableFuture.completedFuture(result);
  }
}
Enter fullscreen mode Exit fullscreen mode

What we did here is create 3 different Async methods. First saveOrderDetails service is a straightforward asynchronous
service, which will start doing the computing asynchronously. If we want to use modern asynchronous Java features
like CompletableFuture, we can achieve it with saveOrderDetailsFuture service. With this service, we can call a thread to wait for the result of an @Async. It should be noted that CompletableFuture.get() will block until the result is available. If we want to perform further asynchronous operations when the result is available, we can use thenApply, thenAccept, or other methods provided by CompletableFuture.

  1. Create a REST controller to trigger the asynchronous method:
@RestController
public class AsyncController {

 private final OrderService orderService;

  public OrderController(OrderService orderService) {
    this.orderService = orderService;
  }

  @PostMapping("/process")
  public ResponseEntity<Void> process(@RequestBody Order order) throws InterruptedException {
    System.out.println("PROCESSING STARTED");
    orderService.saveOrderDetails(order);
    return ResponseEntity.ok(null);
  }

  @PostMapping("/process/future")
  public ResponseEntity<String> processFuture(@RequestBody Order order) throws InterruptedException, ExecutionException {
    System.out.println("PROCESSING STARTED");
    CompletableFuture<String> orderDetailsFuture = orderService.saveOrderDetailsFuture(order);
    return ResponseEntity.ok(orderDetailsFuture.get());
  }

  @PostMapping("/process/future/chain")
  public ResponseEntity<Void> processFutureChain(@RequestBody Order order) throws InterruptedException, ExecutionException {
    System.out.println("PROCESSING STARTED");
    CompletableFuture<String> computeResult = orderService.compute(order);
    computeResult.thenApply(result -> result).thenAccept(System.out::println);
    return ResponseEntity.ok(null);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, when we access the /process endpoint, the server will return a response right away, while
the saveOrderDetails() continues to execute in the background. After 2 seconds, the service will complete. Second endpoint - /process/future will use our second option which is CompletableFuture In this case after 5 seconds, the service will complete, and store the result in CompletableFuture we can further use future.get() to access the result. In the last endpoint -/process/future/chain, we optimized and used asynchronous computations. Controller using the same service method for CompletableFuture, however right after the future, we are using thenApply, thenAccept methods. The server returns a response right away, we do not need to wait for 5 seconds, and computation will be done background. The most important point, in this case, is a call to async service, in our case compute() must be done from the outside of the same class. If we use @Async on a method and call it within the same class, it won't work. This is because Spring uses proxies to add asynchronous behavior, and calling the method internally bypasses the proxy. To make it work, we can either:

  • Move the @Async methods to a separate service or component.
  • Use ApplicationContext to get the proxy and call the method on it.

Conclusion

The @Async annotation in Spring is a powerful tool for enabling asynchronous processing in applications. By using @Async, we don't need to go into the complexities of concurrency management and multithreading to enhance the responsiveness and performance of our application. But to decide when to use @Async or go with alternative concurrency
utilities, it's important to know its limitations and use cases. This is the link for the project used on this blog.

You can check my blog website as well: https://blog.ilkinmehdiyev.com/posts/understanding-async-java

Top comments (0)