제이온

[Spring] How to Use @Async

Created: 2024-04-25

Created: 2024-04-25 22:33

Java Asynchronous Processing

Before looking at Spring @Async, it is essential to understand the concepts of synchronous, asynchronous, and multithreading. Assuming you are familiar with these concepts, let's explore pure Java asynchronous processing methods through code. If you are familiar with Java threads, you can skip this chapter.



If you create a function that simply receives a message and prints it using a synchronous method, you can write the code as shown above. If you change this to a multithreading asynchronous method, you can write the source code as follows.



However, this method is very dangerous because you cannot manage the threads. For example, if 10,000 calls are made simultaneously, 10,000 threads need to be created in a very short time. The cost of creating threads is not insignificant, which can negatively impact program performance and even cause OOM errors. Therefore, you need to implement a thread pool to manage threads, and Java provides the ExecutorService class.



The total number of threads is limited to 10, and we can now correctly perform the desired multithreading asynchronous processing. However, with this method, you must apply the ExecutorService's submit() method to each method you want to process asynchronously, requiring repetitive modification work. In other words, if you initially want to change a method written with synchronous logic to asynchronous, you inevitably need to modify the logic of the method itself.


Spring @Async

Simple Method


Simply attach the @EnableAsync annotation above the Application class and the @Async annotation above the synchronous logic method you want to process asynchronously, and you're done. However, this method has the problem of not managing threads. This is because the default setting of @Async is to use SimpleAsyncTaskExecutor, which is not a thread pool but simply creates threads.


Using a Thread Pool

First, remove @EnableAsync from the Application class. If the Application class has @EnableAutoConfiguration or @SpringBootApplication settings, it reads the threadPoolTaskExecutor bean information from the SpringAsyncConfig class (which we will create below) at runtime because it is configured with @Configuration.



You can set the core and max sizes. At this point, you might expect it to operate with the initial core size and then increase the number of threads to the max size if it can't handle any more tasks, but that's not the case.


Internally, it creates a LinkedBlockingQueue with a size of Integer.MAX_VALUE, and if the core-sized threads cannot handle the tasks, they will wait in the Queue. Once the Queue is full, it will then create threads up to the max size to process the tasks.


If you find setting the Queue size to Integer.MAX_VALUE too burdensome, you can set queueCapacity. With the settings above, it will initially process with 3 threads, and if the processing speed slows down, it will put tasks in a 100-sized Queue, and if more requests come in, it will create up to 30 threads to process the tasks.


Once the thread pool settings are complete, you can simply attach the name of the bean to the method with the @Async annotation.



If you want to configure multiple types of thread pools, create multiple bean creation methods like threadPoolTaskExecutor(), and when setting @Async, insert the desired thread pool bean.


Return Type and Returned Form

**When there is no return value**

This is the case when the method to be processed asynchronously does not need to return a result. In this case, you can set the return type of the @Async annotation to void.


**When there is a return value**

You can use Future, ListenableFuture, and CompletableFuture types as the return type. You can wrap the return form of the asynchronous method in new AsyncResult().


[Future]


future.get() blocks and waits until the request result arrives. This makes it a synchronous blocking method, resulting in poor performance. Future is typically not used.


[ListenableFuture]


ListenableFuture allows you to process tasks in a non-blocking manner through callbacks. The first parameter of the addCallback() method defines the task completion callback method, and the second parameter defines the task failure callback method. Note that since the core threads of the thread pool are set to 3, you can see that the "Task Start" message is printed 3 times initially.



[CompletableFuture]

While ListenableFuture alone can implement non-blocking logic, if you need a callback within a callback, it can lead to very complex code known as callback hell.


<span class="image-inline ck-widget" contenteditable="false"><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F9f152db8-c015-43cd-bf13-85c594f5f218%2FUntitled.png?table=block&id=268ac0bc-ca7b-4bcb-b11b-ac611a5038b2&spaceId=b453bd85-cb15-44b5-bf2e-580aeda8074e&width=2000&userId=80352c12-65a4-4562-9a36-2179ed0dfffb&cache=v2" alt="Untitled" style="aspect-ratio:2000/1455;" width="2000" height="1455"></span>


Of course, we won't be covering CompletableFuture in detail in this session, so if you're curious about the code that handles complex callbacks, please refer to the link in the source below.



It has better readability than defining callbacks in ListenableFuture and fully performs non-blocking functions. Therefore, when using @Async, it is recommended to use CompletableFuture if you need a return value.


Advantages of @Async

Developers can write methods synchronously and, if they want asynchronous behavior, simply attach the @Async annotation above the method. This allows for well-maintained code that can handle both synchronous and asynchronous operations.


Precautions for @Async

To use the @Async feature, you need to declare the @EnableAsync annotation. If you don't configure it separately, it will operate in proxy mode. In other words, all asynchronous methods operating with the @Async annotation will follow the restrictions of Spring AOP. For details, please refer tothis post.


  • Even if you attach @Async to a private method, AOP will not work.
  • If methods within the same object call each other, AOP will not work.


Sources

Comments0