Spring Framework: asynchronous methods with @Async, Future and TaskExecutor

logo spring

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

  1. Do (well) several things at the same time
  2. Asynchronous tasks with @Async
  3. A common mistake (and how to avoid it)
  4. Getting the response of asynchronous tasks with Future
  5. Spring Data and @Async
  6. Custom TaskExecutors
  7. 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 @SpringBootApplication) 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.

Spring bean proxy

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 FutureTask instance.

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.

2 comentarios sobre “Spring Framework: asynchronous methods with @Async, Future and TaskExecutor

  1. 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);

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.