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 examine in this post. Make the most of it! 🚀

>> Read this post in Spanish here <<

Table of contents

  1. Do (well) several things at the same time
  2. Asynchronous tasks with @Async
  3. Getting the response of asynchronous tasks with Future
  4. Spring Data and @Async
  5. Creating custom TaskExecutors
  6. 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 getting 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 get 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! Another yellow bar we shortened. 💪

In the present post, we will see how to apply this technique in projects developed with Spring. At the end, we will review some additional features provided by Spring Boot.

Important. Multi-threading can be complex and makes debugging and testing hard. Therefore, you should always check very carefully whether its implementation is beneficial. In our example, it wouldn’t make sense to parallelize queries that run in the order of milliseconds. And if the queries are slow, the first thing to do is to analyze them for possible optimization.

Asynchronous tasks with @Async

In Spring Framework, the above situations are easy to solve: getting a public method of a bean executed 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 is also applicable to classes and interfaces so that all (public) methods will receive the annotation.

Whether you use Spring Boot or not, enabling the @Async annotation processing is essential. You must 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 />

No further action is required; the @Async methods will be asynchronous, but only when they are called from outside the bean. Calling an @Async method from another in the same class will not achieve the desired asynchrony. In these cases, the simplest solution is to refactor to place the methods in different classes.

All this magic is because a TaskExecutor implementation executes the task. Spring automatically creates a bean of this type with the SimpleAsyncTaskExecutor class. Whenever an asynchronous task needs to be executed, this TaskExecutor will run it inside a new Thread object. Creating the Thread is computationally expensive; we will discuss how to optimize it later.

This is the best way to execute asynchronous code in Java. Try not to create a new Thread by hand: it is a «low-level» operation. Instead, use an Executor to run your asynchronous code wrapped inside a Runnable or Callable object. You can get an Executor with the static methods of Executors:

Executors.newSingleThreadExecutor().execute(() -> System.out.println("Hello, I'm Async!!!!"));

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 we want to get the result, we’ll wrap the response in an object of type Future.

There are several implementations, such as FutureTask or the powerful CompletableFuture (*) introduced in Java 8. Spring also provides an implementation called AsyncResult. Although its constructors are private, it has a static method that returns a ListenableFuture. However, AsyncResult and ListenableFuture were deprecated in Spring 6 (November 2022) in favor 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 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. The 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 this, the code snippet exemplifies an inappropriate use of asynchronous tasks. Note that if right after calling an asynchronous task we wait until we read its result, we don’t get the advantage of asynchrony. It is equivalent to working synchronously with an additional handicap: handling asynchrony adds a delay.

Nevertheless, 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 available in Future.

V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

In addition to two arguments 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 is not completed within one second, get will throw a TimeoutException. Since it is a «checked exception», we can’t ignore it: we catch it or declare it in the method. Most likely, we want to catch it and move on.

Speaking of exceptions, get methods throw ExecutionException if the task was aborted 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 thrown by executeAsyncException. 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 the Spring Data repositories support @Async and Future:

@Async
Future<User> findByFirstname(String firstname);

@Async
CompletableFuture<List<User>> findAll(String firstname);

We can declare the return type of the methods that define derived queries with the FutureTask, CompletableFuture, and AsyncResult classes (remember that it is deprecated in Spring Boot 3 and Spring 6). You can also use the Future interface, in which case you will receive the AsyncResult (Spring Boot 2) or CompletableFuture (Spring Boot 3) implementation.

Creating custom TaskExecutors

In general, we’ll want to configure a TaskExecutor according to the needs of each project. So what we will do is create one as a Spring bean in a factory method. This method is neither private nor final, is annotated with @Bean, belongs to a configuration class (@Configuration or @SpringBootAplication), and will return the bean. The name of a bean built in this way matches the name of the method unless we name it using the name property of @Bean.

Let’s get to work. In the following example, I have created a custom TaskExecutor with ThreadPoolTaskExecutor. Unlike SimpleAsyncTaskExecutor, this implementation contains a pool of reusable threads to avoid the overhead of creating them. This is the same technique Spring Boot uses with database connections.

@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.

The execution of an asynchronous task requires a thread from the pool. If one is idle, it is taken. If not, a new one is created if it doesn’t lead to the pool containing more than the maximum allowed number of threads. This means that up to 10 running tasks (the maximum size of the pool) can simultaneously be in the pool. When this limit is reached, new requests are queued up, waiting for a thread to be released. And when the queue reaches 20, the next requests will be dismissed.

Do you need different TaskExecutors? No problemo. You can 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

We can also create as many TaskExecutors as we want with factory methods. But now we 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();
}

As you can guess from the configuration parameters, we set up a TaskExecutor based on a thread pool.

Other posts in English

Spring Framework: event handling

Spring Boot and JSP integration

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

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