Spring Framework supports asynchronous execution of public methods of beans. They will be executed on their own thread without blocking the execution of the code that calls them. If you leverage this feature where appropriate, you will effortlessly boost the performance of your code. That’s the power of the @Async
annotation that we will discuss in this post. Make the most of it! 🚀
>> Read this post in Spanish here <<
Contents
- Do (well) several things at the same time
- Asynchronous tasks with @Async
- A common mistake (and how to avoid it)
- Getting the response of asynchronous tasks with Future
- Spring Data and @Async
- Custom TaskExecutors
- TaskExecutor in Spring Boot
Do (well) several things at the same time
We humans are terrible at multitasking. Instead, it is one of the things computers and similar devices are best at. Sometimes we forget about it and overlook enhancements that can increase the performance of our software.
Imagine that you have to implement a business process that generates a report. You gather the information with two somewhat slow database queries: you run one, then the other, and populate the report. If each query takes two seconds, the whole task of fetching the data will take four seconds.
The illustration below depicts the time sequence of the report generation. The yellow block represents the total duration; the blue blocks reflect the time taken by each query.
So far, so typical, but what if the queries were independent? That is, you don’t need to know the result of one to run the other. This implies that you can run each query separately in its own thread. You call them and wait for the results to come in.
Let’s see if this new approach is better.
We made the yellow bar shorter because now the two seconds of each query more or less coincide in time. It will take less than four seconds to fetch the data. It won’t be reduced to two because of the time-consuming concurrency management, but if we do it right and the use case is suitable, this delay is more than worth it.
Without going into details, I would like to clarify that a thread is a small process that runs under the umbrella of the process that represents the whole application, so it can use its resources. In Java, threads are modeled with the Thread
class, and we work with them using the classes and interfaces from the java.util.concurrent
package.
Another common scenario. You have a business process with a step that sends an email. This is something that usually takes several seconds. But imagine this action is unimportant and you don’t care about the result. So you might as well request the email to be sent and go on with your work without waiting for the email to be sent (or not).
Great! We have shortened another yellow bar. 💪
In this post you will learn to apply this technique in projects developed with Spring. At the end, we will review some additional features provided by Spring Boot.
Warning. Asynchronous programming is not a silver bullet. Multi-threading can be complex and makes debugging and testing hard. Like any optimization technique, you must carefully check whether its implementation is beneficial. In our example, it wouldn’t make sense to parallelize queries that run in the order of milliseconds. First, measure the performance; then, optimize if need be. Remember this quote:
“The first lesson of optimization is don’t optimize.”
(Chris Zimmerman, The Rules of Programming, Ed. O’Reilly Media, 2023).
Asynchronous tasks with @Async
In Spring Framework, the above scenarios are easy to implement. Executing a bean method in a new thread is as simple as using the @Async
annotation:
@Service
public class AsyncService {
@Async
public void executeAsync() {
System.out.println("Hello, I'm async!!!");
}
These kind of methods —marked with @Async
— are called asynchronous tasks.
@Async
is applicable to interface methods. It can also annotate classes and interfaces so that all methods will receive the annotation.
Whether you use Spring Boot or not, you must enable the @Async
annotation processing. Just mark a configuration class (one annotated with @Configuration
or @SpringBootApplicatio
n) with @EnableAsync
. Otherwise, @Async
will be ignored.
@Configuration
@EnableAsync
public class AsyncApplication {
The same configuration in XML:
<task:annotation-driven />
How does @Async
work under the hood? A TaskExecutor
implementation executes the task. Spring automatically creates a bean of that type with the SimpleAsyncTaskExecutor
class. Whenever an asynchronous task needs to be executed, this TaskExecutor
will run it inside a new Thread
object.
Note that creating a Thread
is computationally expensive. Later we will discuss how to improve the performance by customizing a TaskExecutor
.
Classes that implement the Executor
interface, a superinterface of TaskExecutor
, provide the best way to execute asynchronous code in Java. Don’t create a new Thread
by hand—it is a low-level operation. Instead, use an Executor
instance to run your asynchronous code wrapped inside a Runnable
or Callable
object. You obtain an Executor
with the static methods of the Executors
class:
Executors.newSingleThreadExecutor()
.execute(() -> System.out.println("Hello, I'm Async!!!!"));
A common mistake (and how to avoid it)
You should be aware of a crucial detail because it is a potential source of errors. @Async
only works when the method annotated with it is invoked from a different Spring bean. This limitation —annotations that only work on methods invoked between different beans— is also present in other Spring annotations, such as @Transactional
and @Cacheable
. Why is this happening? 🤔
Spring beans are proxy objects that enhance the real objects they hold with capabilities such as those provided by the above annotations. For those capabilities to be applied, calls to the methods of the real object must go through the proxy (green icon in the following picture). Capabilities that won’t work when the calls are performed directly between the methods of the real object wrapped by the proxy (red icon) or, in other words, in our code we invoke a method of the class itself.
Two alternatives let us to work around the above limitation:
- Refactor the code to split into distinct classes the methods involved. This is what I usually do.
- Inject the class into itself:
@Service
public class MyUserCaseImpl {
@Autowired
@Lazy
private MyUserCaseImpl _self;
The MyUserCaseImpl
class contains a reference to itself, but the calls to the _self
methods go through the proxy so that the proxy can do its magic.
The above technique works even if MyUserCaseImpl
is a singleton bean (there is only one and it is always the same object). This is because this peculiar self-injection is supported by Spring. That said, the injection must be of type @Lazy
; otherwise, you will see this error:
Requested bean is currently in creation: Is there an unresolvable circular reference?
The message is clear: creating a MyUserCaseImpl
object requires a MyUserCaseImpl
object. Pretty crazy, right? 🤪
@Lazy
solves the problem by injecting into the _self
field at the time of creating the MyUserCaseImpl
object a special proxy. Later, the proxy will be replaced by the real bean the first time you call a method of _self
.
Getting the response of asynchronous tasks with Future
The return of the asynchronous tasks is a key point. They can return anything or void
, but if you want to get the result (type V
), you must wrap the response in an object of type Future<V>
.
There are several implementations, such as FutureTask<V>
or the powerful CompletableFuture<V>
(*) introduced in Java 8. Spring also provides an implementation named AsyncResult<V>
. Although its constructors are private, it has a static method that returns a ListenableFuture<V>
. However, AsyncResult
and ListenableFuture
were deprecated in Spring Boot 3 \ Spring 6 (November 2022) to encourage the usage of CompletableFuture
.
(*) This class is more than just a Future
. It brings to Java the promise-based asynchronous programming style so popular in JavaScript. The great Venkat Subramaniam explains it in one of his superb presentations.
Here’s a first example:
@Async
Future<String> executeAsync() {
return CompletableFuture.completedFuture("Hi there!!");
}
Future<String> resultFuture = asyncService.executeAsync();
String message = resultFuture.get();
System.out.println(message);
The resultFuture
object represents the result of the asynchronous task, which will be known once the task finishes. Its get
method returns that result (I will talk about the exceptions it can throw later). If it doesn’t yet exist when we invoke it, we have to wait to get it. The execution of the code is blocked for as long as necessary—as long as the task takes.
Because of the above behavior, the code snippet exemplifies an inappropriate use of asynchronous tasks. Note that if right after calling an asynchronous task you wait until you read its result, you don’t get the advantage of asynchrony. It is equivalent to working synchronously with an additional handicap: handling asynchrony adds a delay.
Yet one scenario comes to my mind in which the above may make sense: limiting the maximum duration of the execution of a method with a timeout. This is possible thanks to the other get
method exposed by Future
:
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
In addition to the two parameters that set the timeout value, the method’s signature adds the exception TimeoutException
, whose name is very suspicious…
Future<String> resultFuture = asyncService.executeAsync();
String message;
try {
message = resultFuture.get(1, TimeUnit.SECONDS);
} catch (TimeoutException e) {
message = "timeout!!!";
}
System.out.println(message);
The code is clear: if the task doesn’t finish within one second, get
will throw a TimeoutException
. Since it is a checked exception, you can’t ignored it—you catch it or declare it in the method. Most likely, you want to catch it and act accordingly.
Speaking of exceptions, get
methods throw ExecutionException
if the task was ended due to an exception:
@Async
Future<String> executeAsyncException() {
throw new RuntimeException("ha ha ha");
}
Future<String> stringFuture = asyncService.executeAsyncException();
String message;
try {
message = stringFuture.get();
} catch (ExecutionException e) {
message = e.getCause().getClass() + " " + e.getCause().getMessage();
}
System.out.println(message);
The cause
field of ExecutionException
holds the exception raised by executeAsyncException
, so the previous code prints the following message:
class java.lang.RuntimeException ha ha ha
The other exception the get
methods can throw is InterruptedException
. It is raised when the Thread#interrupt
method is called to request to stop the thread corresponding to that Thread
object, which may or may not be successful. In Future
, the interrupt is requested by invoking cancel
with true
. However, depending on the Future
implementation, interrupt may never be called. Such is the case of AsyncResult
:
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
Spring Data and @Async
It is worth mentioning that Spring Data repositories support @Async
and Future
:
@Async
Future<User> findByFirstName(String firstName);
@Async
CompletableFuture<List<User>> findAll(String firstName);
Declare the return type of the methods that define the queries with the CompletableFuture
class or the ListenableFuture
interface. Remember that the latter was deprecated in Spring Boot 3 \ Spring 6. You may also use the Future
interface, in which case you will receive a
instance.FutureTask
Custom TaskExecutors
In general, you will want to configure a TaskExecutor
according to the requirements of each project. So what you will do is create one as a Spring bean in a factory method. A method of this type must follow some rules:
- it is neither private nor final
- it can receive any bean as a method parameter
- it is annotated with
@Bean
- it belongs to a configuration class (
@Configuration
or@SpringBootAplication
) - it returns the bean
- its name is the name of the bean unless you name it using the
name
property of@Bean
In the following example, I build a custom TaskExecutor
with ThreadPoolTaskExecutor
. Unlike SimpleAsyncTaskExecutor
, this implementation contains a pool of reusable threads to avoid the overhead of creating them.
@Bean
TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2); //default: 1
executor.setMaxPoolSize(10); //default: Integer.MAX_VALUE
executor.setQueueCapacity(20); // default: Integer.MAX_VALUE
executor.setKeepAliveSeconds(120); // default: 60 seconds
executor.initialize();
return executor;
}
The equivalent XML configuration is as follows:
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="2"/>
<property name="maxPoolSize" value="10"/>
<property name="queueCapacity" value="20"/>
<property name="keepAliveSeconds" value="120"/>
</bean>
The example configures a dynamic thread pool with a size between 2 and 10 and a request queue of 20. An idle thread will remain in the pool for up to 120 seconds. You should carefully choose these values for every project.
How taskExecutor
works? The execution of an asynchronous task requires a thread from the pool. If there is an idle thread, it is taken; otherwise, a new one is created if it doesn’t lead to the pool containing more than the maximum allowed number of threads. This behavior means up to 10 tasks (the pool’s maxiumun size) can run simultaneously. When this limit is reached, new tasks are queued up, waiting for a thread to be available. And when the queue reaches 20 (the queue capacity), the next requests will be dismissed.
Do you need different TaskExecutors
? No problemo ! Create as many as you need…
@Bean
TaskExecutor taskExecutor2() {
....
}
…and set in @Async
the one you want to use:
@Async("taskExecutor2")
public void doSomething() {
...
}
If you don’t set the name in @Async
, Spring will use the default TaskExecutor
. When more than one TaskExecutor
exists, the default will be the one named taskExecutor or the one marked with @Primary
.
TaskExecutor in Spring Boot
If you use Spring Boot, you can set the default TaskExecutor
with the spring.task.execution.pool
properties in the application.properties
(or application.yml
) file:
spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=10
spring.task.execution.pool.queue-capacity=20
spring.task.execution.pool.keep-alive=120
Of course, you can create as many TaskExecutors
as you want via factory methods. But now you have a little help from the TaskExecutorBuilder
class:
@Bean
public TaskExecutor taskExecutor(TaskExecutorBuilder builder) {
return builder.corePoolSize(2)
.maxPoolSize(10)
.queueCapacity(20)
.keepAlive(Duration.ofSeconds(120))
.build();
}
The above method builds a TaskExecutor
based on a thread pool.
Other posts in English
Spring Framework: event handling
Spring Boot and JSP integration
Spring Boot testing: Docker with Testcontainers and JUnit 5. MySQL and other images.
Hi Thanks for the tutorial, In Spring Data repositories if I need to make save method Async how shall I do that?
I know I need to add @Async annotation, but this require to return the Future. How shall I return the Future for save method ?
S save (S entity);You can add the method to a repository:
@Async
Future save(User user);