Spring Framework permite la ejecución asíncrona de métodos públicos de los beans (los objetos gestionados por el sistema de inyección de dependencias). Se ejecutarán en su propio hilo o hebra sin bloquear al código que los llama. Si utilizas esta capacidad donde resulte adecuada, impulsarás el rendimiento de tu código sin esfuerzo alguno. Tal es el poder de la anotación @Async
que veremos en este tutorial. ¡Sácale partido! 🚀
>> Read this post in English here <<
Tabla de contenidos
- Haz (bien) varias cosas a la vez
- Tareas asíncronas con @Async
- La respuesta de las tareas asíncronos con Future
- Spring Data y @Async
- Creando TaskExecutor personalizados
- TaskExecutor en Spring Boot
Haz (bien) varias cosas a la vez
Los humanos somos nefastos realizando varias tareas al mismo tiempo. En cambio, es una de las cosas que mejor se les da a las computadoras y máquinas similares. A veces nos olvidamos de ello y pasamos por alto mejoras que pueden aumentar el rendimiento de nuestros programas.
Imagina que debes implementar un proceso de negocio que genere un informe: recopilas la información con dos consultas un tanto lentas a bases de datos, ejecutas una, luego la otra, y rellenas el informe. Si cada consulta tarda dos segundos, la tarea completa de obtención de los datos consume cuatro segundos.
La siguiente ilustración muestra la secuencia temporal de la generación del informe. El bloque amarillo representa la duración total; los azules, la tardanza de las consultas.

Hasta aquí, todo normal (y obvio). ¿Y si las consultas fueran independientes? Es decir, no se necesita conocer el resultado de una para poder ejecutar la otra. Esto implica que puedes realizar cada consulta de manera separada en un hilo de ejecución propio. Las pides y aguardas a la recepción de los datos.
Veamos si este nuevo enfoque es mejor.

Hemos conseguido que la barra amarilla sea más corta porque ahora los dos segundos de cada consulta coinciden, más o menos, en el tiempo. La obtención de los datos tardará menos de cuatro segundos. No se reducirá a dos debido a que la gestión de la concurrencia consume tiempo, pero si lo hacemos bien y el caso de uso se presta a ello, esta demora nos compensa sobremanera.
Sin entrar en detalles, me gustaría aclarar que un hilo es un pequeño proceso que se ejecuta bajo el paraguas del proceso que representa a toda la aplicación, por lo que puede usar sus recursos. En Java, los hilos se modelan con la clase Thread
y se trabajan con las clases e interfaces del paquete java.util.concurrent
. Trataremos varias de ellas.
Otro escenario típico. Tienes un proceso de negocio con un paso que envía un correo electrónico, acción que suele tardar varios segundos. Imagina que esta acción no es importante y su resultado te da igual. Así que mejor solicitar el envío del correo y seguir con lo tuyo, sin esperar a que se efectúe el envío (o falle).

¡Genial! Otra barra amarilla que acortamos.
En el resto del tutorial abordaremos cómo aplicar esta técnica en proyectos desarrollados con Spring. Al final revisaremos algunas funcionalidades adicionales ofrecidas por Spring Boot.
Importante. La programación multihilo puede ser bastante compleja y dificulta la depuración y el testeo. Comprueba siempre con sumo cuidado si su aplicación resulta beneficiosa. En el ejemplo, no tendría sentido paralelizar consultas que se mueven en el orden de los milisegundos, y si las consultas fueran lentas, lo primero será revisarlas a fondo por si pudieras optimizarlas.
Tareas asíncronas con @Async
En Spring Framework las situaciones anteriores son fáciles de resolver: conseguir que un método público de un bean se ejecute en un nuevo hilo es tan sencillo como añadirle la anotación @Async
.
@Service
public class AsyncService {
@Async
public void executeAsync() {
System.out.println("Hello, I'm async!!!");
}
A este tipo de métodos se les denomina tareas asíncronas.
@Async
es aplicable a métodos de interfaces. También a clases e interfaces completas, de tal modo que todos los métodos públicos recibirán la anotación.
Tanto si usas Spring Boot como si no, debes activar el procesamiento de @Async
marcando con @EnableAsync
una clase de configuración (esto es, una anotada con @Configuration
o @SpringBootApplication
). De lo contrario, @Async
será ignorada.
@Configuration
@EnableAsync
public class AsyncApplication {
La equivalencia en XML:
<task:annotation-driven />
No se requiere nada más. Los métodos @Async
de un bean serán asíncronos, pero solo cuando se llamen desde un bean distinto. Invocando un método @Async
desde otro método situado en el mismo bean no obtendremos la deseada asincronía. Esta limitación, también presente en otras anotaciones como @Transactional
y @Cacheable
, la explico aquí, incluyendo posibles soluciones.
¿Cómo se las apaña Spring para que los métodos @Async
se ejecuten en un hilo? Aunque no vamos a indagar demasiado en esta cuestión, es importante saber que una implementación de TaskExecutor
ejecuta las tareas asíncronas. Spring crea de manera automática un bean de este tipo de la clase SimpleAsyncTaskExecutor
. Cada vez que haya que ejecutar una tarea asíncrona, este TaskExecutor
lo hará dentro de un nuevo Thread
. La creación del hilo es costosa; veremos cómo mejorar el rendimiento más adelante.
Esta forma de proceder es la mejor manera de ejecutar código asíncrono en Java. Procura no crear a mano un nuevo Thread
, es una acción de «bajo nivel». En vez de ello, utiliza un Executor
para ejecutar tu código asíncrono dentro de un Runnable
o Callable
. Puedes obtener un Executor
con los métodos estáticos de Executors
:
Executors.newSingleThreadExecutor().execute(() -> System.out.println("Hello, I'm Async!!!!"));
La respuesta de las tareas asíncronos con Future
Un aspecto fundamental es el retorno de las tareas asíncronas. Pueden devolver cualquier cosa o void
, pero si quieres recibir el resultado, envuelve la respuesta en un objeto de la interfaz Future
.

Cuentas con varias implementaciones, como FutureTask
o la poderosa CompletableFuture
(*) introducida en Java 8. Asimismo, Spring provee una implementación llamada AsyncResult
. Si bien sus constructores son privados, tiene un método estático que devuelve un ListenableFuture
. De todas formas, AsyncResult
y ListenableFuture
se declararon obsoletas en Spring 6 (noviembre de 2022) para favorecer el empleo de CompletableFuture
.
(*) Esta clase es mucho más que un Future
: posibilita en Java el estilo de programación asíncrona basado en promesas (promises) tan común en el mundo JavaScript. El genial Venkat lo explica en una de sus magníficas presentaciones.
Veamos un primer ejemplo:
@Async
Future<String> executeAsync() {
return CompletableFuture.completedFuture("Hi there!!");
}
Future<String> resultFuture = asyncService.executeAsync();
String message = resultFuture.get();
System.out.println(message);
El objeto resultFuture
representa el resultado de la tarea asíncrona, el cual no será conocido hasta que la tarea termine. El método get
devuelve ese resultado (más adelante hablaré de las excepciones que puede lanzar). Si cuando se invoca todavía no existe, quedamos a la espera de recibirlo. La ejecución del código queda bloqueada el tiempo que sea necesario (lo que tarde la tarea).
Debido a esto último, el fragmento de código ejemplifica un uso incorrecto de las tareas asíncronas. Date cuenta de que si justo después de llamar a una tarea asíncrona esperas a leer su resultado, no obtienes beneficio de la asincronía. Equivale a trabajar de forma síncrona añadiendo un hándicap: la gestión de la asincronía añade una demora.
Sin embargo, se me ocurre un (rebuscado) escenario en el que lo anterior puede tener sentido: acotar la duración máxima de la ejecución de un método con un timeout. Esto es posible gracias al otro método get
presente en Future
:
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
Además de dos parámetros que establecen el valor del timeout, la signatura del método añade la excepción TimeoutException
, de nombre sospechoso…
Future<String> resultFuture = asyncService.executeAsync();
String message;
try {
message = resultFuture.get(1, TimeUnit.SECONDS);
} catch (TimeoutException e) {
message = "timeout!!!";
}
System.out.println(message);
El código es claro: si en el plazo de un segundo la tarea no finalizó, get
lanzará un TimeoutException
. Al ser una checked exception (esto es, no es de tipo Runtime), no puedes ignorarla: o la capturas o la declaras en el método. Lo más probable es que quieras capturarla y obrar en consecuencia.
Hablando de excepciones, los métodos get
lanzan ExecutionException
si la tarea finalizó por motivo de una excepción:
@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);
El atributo cause
de ExecutionException
contiene la excepción lanzada por executeAsyncException
, así que el código anterior escribe lo siguiente:
class java.lang.RuntimeException ha ha ha
La otra excepción que pueden lanzar los get
es InterruptedException
. Se produce cuando se llama al método Thread#interrupt
para solicitarse la detención del hilo correspondiente a ese objeto Thread
, petición que puede o no hacerse efectiva. En Future
, la interrupción se pide invocando a cancel
, dándole como argumento el valor true
. No obstante, en función de la implementación de Future
, es posible que interrupt
nunca sea llamado. Es el caso de AsyncResult
:
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
Spring Data y @Async
Merece la pena indicar que los repositorios de Spring Data son compatibles con @Async
y Future
:
@Async
Future<User> findByFirstName(String firstName);
@Async
CompletableFuture<List<User>> findAll(String firstName);
Declaramos el retorno de los métodos que definen consultas con las clases FutureTask
, CompletableFuture
y AsyncResult
(recuerda que está obsoleta en Spring Boot 3 \ Spring 6). También podemos usar la interfaz Future
, en cuyo caso recibiremos la implementación AsyncResult
(Spring Boot 2) o CompletableFuture
(Spring Boot 3).
Creando TaskExecutor personalizados
Por lo común, querrás configurar un TaskExecutor
de acuerdo a las necesidades de cada proyecto. Debes crearlo como un bean de Spring en un método factoría. Un método de este tipo debe cumplir las siguientes reglas:
- no es ni privado ni final
- está anotado con
@Bean
- pertenece a una clase de configuración (
@Configuration
o@SpringBootAplication
) - recibe como parámetros los beans que necesite
- devuelve el bean
- el nombre de un bean construido de este modo coincide con el del método, salvo que lo nombremos empleando la propiedad
name
de@Bean
.
Manos a la obra. En el siguiente ejemplo he creado con ThreadPoolTaskExecutor
un TaskExecutor
personalizado. A diferencia de SimpleAsyncTaskExecutor
, esta implementación contiene lo que se conoce como «pool de hilos»: un conjunto de hilos reutilizables con el fin de ahorrar el coste de su creación. Es la misma técnica que Spring Boot usa con las conexiones a las bases de datos.
@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;
}
La configuración equivalente en XML:
<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>
Nota. Es recomendable definir los parámetros de configuración fuera del código siguiendo el tutorial Ficheros de propiedades en Spring.
El ejemplo configura un pool de hilos dinámico con un tamaño comprendido entre 2 y 10, y una cola de peticiones limitada a 20. Un hilo sin uso permanecerá en el pool un máximo de 120 segundos. Escoge con cuidado estos valores para cada proyecto.
¿Cómo funciona taskExecutor
? La ejecución de una tarea asíncrona requiere un hilo del pool. Si hay alguno desocupado, se usa; si no, se crea uno nuevo, siempre y cuando no provoque que el pool contenga más hilos de los permitidos. Esto significa que no pueden coincidir más de 10 tareas asíncronas en ejecución (el tamaño máximo del pool). Al llegarse a ese límite, las nuevas peticiones quedan en la cola en espera de la liberación de algún hilo. Y cuando en la cola se acumulen 20 (su capacidad máxima) las siguientes solicitudes se descartarán.
¿Necesitas varios TaskExecutor
distintos? Creálos…
@Bean
TaskExecutor taskExecutor2() {
....
}
…e indica en @Async
el que quieras usar:
@Async("taskExecutor2")
public void doSomething() {
...
}
Si no declaras el nombre en @Async
, Spring elegirá el TaskExecutor
predeterminado. Cuando exista más de un TaskExecutor
, el predeterminado será aquel denominado taskExecuto
o bien el marcado con @Primary
.
TaskExecutor en Spring Boot
En Spring Boot puedes configurar el TaskExecutor
predeterminado con las propiedades de tipo spring.task.execution.pool
en el fichero application.properties
(o application.yml
):
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
También puedes crear múltiples TaskExecutor
con métodos factoría. Ahora cuentas con la pequeña ayuda de la clase TaskExecutorBuilder
:
@Bean
public TaskExecutor taskExecutor(TaskExecutorBuilder builder) {
return builder.corePoolSize(2)
.maxPoolSize(10)
.queueCapacity(20)
.keepAlive(Duration.ofSeconds(120))
.build();
}
Como podrás deducir por los parámetros de configuración, en los dos ejemplos anteriores se definió un TaskExecutor
basado en un pool de hilos.
Otros tutoriales relacionados con Spring
Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA
Spring Boot: Gestión de errores en aplicaciones web y REST
Testing en Spring Boot con JUnit 45. Mockito, MockMvc, REST Assured, bases de datos embebidas
Spring JDBC Template: simplificando el uso de SQL