Concurrencia en Quarkus
Vídeo sobre la historia de los hilos hasta llegar a los hilos virtuales.
Vídeo de la demo explicando las pruebas de concurrencia según cada enfoque:
📚 Introducción
Este proyecto es una demostración educativa construido por Willem Jan Glerum compara 4 enfoques diferentes de concurrencia en Quarkus para implementar una API REST que simula un barista preparando bebidas y guardándolas en base de datos.
Github Repository: https://github.com/joedayz/quarkus-virtual-threads
Requisitos:
Java 26+
Maven 3.9.x+
Objetivo del Proyecto
Simular un barista que:
Prepara café (simula una operación que tarda 3 segundos)
Guarda el resultado en base de datos
Maneja múltiples pedidos de forma concurrente
Cada módulo implementa la misma funcionalidad pero con un estilo de concurrencia diferente, permitiendo comparar sus ventajas y desventajas.
🏗️ Arquitectura del Proyecto
El proyecto está organizado en 4 módulos principales:
src/main/java/nl/wjglerum/
├── _01_blocking/ # Enfoque tradicional bloqueante
├── _02_reactive/ # Programación reactiva
├── _03_virtual/ # Virtual Threads
└── _04_structured/ # Structured Concurrency
Cada módulo contiene:
Resource: Endpoints REST (
*BeverageResource.java)Bartender: Lógica de negocio que simula preparar bebidas (
*Bartender.java)Beverage: Modelo de datos (
*Beverage.java)Repository: Persistencia en base de datos (
*BeverageRepository.java)
📦 Módulo 1: Blocking (Bloqueante)
Descripción
Enfoque tradicional usando threads de plataforma del sistema operativo con código bloqueante.
Características Técnicas
Threads: Threads de plataforma (OS threads)
Base de datos: Hibernate ORM (bloqueante)
Código: Estilo imperativo tradicional
Anotación:
@Transactional
Cómo Funciona
public BlockingBeverage get() {
Thread.sleep(3_000); // Bloquea el thread actual
return new BlockingBeverage("Blocking coffee");
}
Cada request consume un thread del pool (limitado, típicamente ~200 threads)
Si llegan muchas requests simultáneas, se agotan los threads
El servidor puede dejar de responder cuando se satura
Endpoints Disponibles
| Endpoint | Descripción | Tiempo Estimado |
GET /beverage/blocking | Una bebida | ~3 segundos |
GET /beverage/blocking/sequential | Tres bebidas secuencialmente | ~9 segundos |
GET /beverage/blocking/parallel | Tres bebidas en paralelo | ~3 segundos |
Ejemplo de Código: Paralelo
@GET
@Path("/parallel")
public List<BlockingBeverage> getBeveragesParallel() {
var beverage1 = executor.submit(bartender::get);
var beverage2 = executor.submit(bartender::get);
var beverage3 = executor.submit(bartender::get);
var beverages = List.of(
beverage1.get(),
beverage2.get(),
beverage3.get()
);
repository.save(beverages);
return beverages;
}
Ventajas
✅ Código simple y fácil de entender
✅ Estilo imperativo tradicional
✅ Fácil de depurar
Desventajas
❌ Limitado por el número de threads de plataforma
❌ No escala bien con muchas requests concurrentes
❌ Puede agotar recursos del servidor
Diagrama de Flujo
Request 1 ──┐
Request 2 ──┤
Request 3 ──┼──> Thread Pool (200 threads) ──> ❌ Se agota
Request 4 ──┤
... ┘
⚡ Módulo 2: Reactive (Reactivo)
Descripción
Programación reactiva usando Mutiny y Hibernate Reactive. No bloquea threads, usa un modelo de eventos.
Características Técnicas
Threads: Event loop (pocos threads)
Base de datos: Hibernate Reactive (no bloqueante)
Código: Estilo reactivo con
Uni<T>Anotación:
@WithTransaction
Cómo Funciona
public Uni<ReactiveBeverage> get() {
return Uni.createFrom()
.item(new ReactiveBeverage("Reactive coffee"))
.onItem().delayIt()
.by(Duration.ofSeconds(3)); // No bloquea el thread
}
Usa
Uni(tipo reactivo de Mutiny)No bloquea threads; usa un modelo de eventos
Hibernate Reactive ejecuta operaciones de forma no bloqueante
delayIt()simula la espera sin bloquear
Endpoints Disponibles
| Endpoint | Descripción | Tiempo Estimado |
GET /beverage/reactive | Una bebida reactiva | ~3 segundos |
GET /beverage/reactive/sequential | Tres bebidas secuencialmente | ~9 segundos |
GET /beverage/reactive/parallel | Tres bebidas en paralelo | ~3 segundos |
Ejemplo de Código: Paralelo
@GET
@Path("/parallel")
public Uni<List<ReactiveBeverage>> getBeveragesParallel() {
var beverage1 = bartender.get();
var beverage2 = bartender.get();
var beverage3 = bartender.get();
return Uni.join()
.all(beverage1, beverage2, beverage3)
.andCollectFailures()
.onItem()
.call(beverages -> repository.save(beverages));
}
Ventajas
✅ Escala muy bien con muchas requests concurrentes
✅ Usa pocos threads (event loop)
✅ Eficiente en recursos
Desventajas
❌ Código más complejo (callbacks, composición)
❌ Curva de aprendizaje más alta
❌ Estilo de programación diferente al tradicional
Diagrama de Flujo
Request 1 ──┐
Request 2 ──┤
Request 3 ──┼──> Event Loop (pocos threads) ──> ✅ Escala bien
Request 4 ──┤
... ┘
🧵 Módulo 3: Virtual Threads (Hilos Virtuales)
Descripción
Usa Virtual Threads (Java 21+) con código bloqueante tradicional, pero sin bloquear threads de plataforma.
Características Técnicas
Threads: Virtual Threads (gestionados por la JVM)
Base de datos: Hibernate ORM (bloqueante, pero sobre virtual threads)
Código: Estilo imperativo tradicional
Anotación:
@RunOnVirtualThread+@Transactional
Cómo Funciona
@RunOnVirtualThread
public VirtualBeverage get() {
Thread.sleep(3_000); // Bloquea el virtual thread, no el OS thread
return new VirtualBeverage("Virtual coffee");
}
Usa
@RunOnVirtualThreadpara ejecutar en virtual threadsCódigo bloqueante (
Thread.sleep) pero sin bloquear threads de plataformaHibernate ORM bloqueante, pero sobre virtual threads
Virtual threads son ligeros (millones posibles)
Endpoints Disponibles
| Endpoint | Descripción | Tiempo Estimado |
GET /beverage/virtual | Una bebida con virtual thread | ~3 segundos |
GET /beverage/virtual/sequential | Tres bebidas secuencialmente | ~9 segundos |
GET /beverage/virtual/parallel | Tres bebidas en paralelo | ~3 segundos |
GET /beverage/virtual/custom | Tres bebidas con executor personalizado | ~3 segundos |
Ejemplo de Código: Paralelo
@Inject
@VirtualThreads
ExecutorService executor;
@GET
@Path("/parallel")
public List<VirtualBeverage> getBeveragesParallel() {
var beverage1 = executor.submit(bartender::get);
var beverage2 = executor.submit(bartender::get);
var beverage3 = executor.submit(bartender::get);
var beverages = List.of(
beverage1.get(),
beverage2.get(),
beverage3.get()
);
repository.save(beverages);
return beverages;
}
Ejemplo de Código: Custom Executor
@GET
@Path("/custom")
public List<VirtualBeverage> getBeveragesCustom() {
var currentThread = Thread.currentThread().getName();
var threadFactory = Thread.ofVirtual()
.name(currentThread + "-virtual-beverage-", 0)
.factory();
try(var executor = Executors.newThreadPerTaskExecutor(threadFactory)) {
var beverage1 = executor.submit(bartender::get);
var beverage2 = executor.submit(bartender::get);
var beverage3 = executor.submit(bartender::get);
var beverages = List.of(
beverage1.get(),
beverage2.get(),
beverage3.get()
);
repository.save(beverages);
return beverages;
}
}
Ventajas
✅ Código bloqueante simple (como Blocking)
✅ Escalabilidad de programación reactiva
✅ Mejor de ambos mundos
✅ Compatible con código existente
Desventajas
❌ Requiere Java 21+
❌ Virtual threads aún son relativamente nuevos
Diagrama de Flujo
Request 1 ──┐
Request 2 ──┤
Request 3 ──┼──> Virtual Threads (millones) ──> ✅ Escala bien
Request 4 ──┤
... ┘
🎯 Módulo 4: Structured Concurrency (Concurrencia Estructurada)
Descripción
Usa Structured Concurrency (Java 21+) con virtual threads para gestionar tareas concurrentes de forma estructurada.
Características Técnicas
Threads: Virtual Threads con Structured Concurrency
Base de datos: Hibernate ORM
Código: Estilo imperativo con
StructuredTaskScopeAnotación:
@RunOnVirtualThread+@Transactional
Cómo Funciona
@RunOnVirtualThread
public List<StructuredBeverage> getBeveragesSimple() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
var beverage1 = scope.fork(() -> bartender.get());
var beverage2 = scope.fork(() -> bartender.get());
var beverage3 = scope.fork(() -> bartender.get());
scope.join(); // Espera a que todas terminen
var beverages = List.of(
beverage1.get(),
beverage2.get(),
beverage3.get()
);
repository.save(beverages);
return beverages;
} // Garantiza que todas las tareas terminan aquí
}
Usa
StructuredTaskScopepara gestionar tareas concurrentesGarantiza que todas las tareas hijas terminan antes de que el scope cierre
Evita "thread leaks" y facilita el manejo de errores
El scope se cierra automáticamente con
try-with-resources
Endpoints Disponibles
| Endpoint | Descripción | Tiempo Estimado |
GET /beverage/structured/simple | Tres bebidas con StructuredTaskScope básico | ~3 segundos |
GET /beverage/structured/custom | Tres bebidas con Joiner personalizado | ~3 segundos |
Ejemplo de Código: Simple
@GET
@Path("/simple")
public List<StructuredBeverage> getBeveragesSimple() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
var beverage1 = scope.fork(() -> bartender.get());
var beverage2 = scope.fork(() -> bartender.get());
var beverage3 = scope.fork(() -> bartender.get());
scope.join();
var beverages = List.of(
beverage1.get(),
beverage2.get(),
beverage3.get()
);
repository.save(beverages);
return beverages;
}
}
Ejemplo de Código: Custom Joiner
@GET
@Path("/custom")
public List<StructuredBeverage> getBeveragesCustom() throws InterruptedException {
var joiner = StructuredTaskScope.Joiner.<StructuredBeverage>allSuccessfulOrThrow();
var currentThread = Thread.currentThread().getName();
var threadFactory = Thread.ofVirtual()
.name(currentThread + "-structured-beverage-", 0)
.factory();
try (var scope = StructuredTaskScope.open(joiner, cf ->
cf.withThreadFactory(threadFactory))) {
scope.fork(bartender::get);
scope.fork(bartender::get);
scope.fork(bartender::get);
var beverages = scope.join(); // Retorna List directamente
repository.save(beverages);
return beverages;
}
}
Ventajas
✅ Mejor control del ciclo de vida de tareas concurrentes
✅ Manejo de errores más robusto
✅ Evita "thread leaks"
✅ Garantiza que todas las tareas terminan correctamente
✅ Código más seguro y predecible
Desventajas
❌ Requiere Java 21+
❌ API aún en preview (puede cambiar)
🔍 Comparación de Enfoques
Tabla Comparativa
| Característica | Blocking | Reactive | Virtual | Structured |
| Código | Simple | Complejo | Simple | Simple |
| Escalabilidad | ❌ Limitada | ✅ Excelente | ✅ Excelente | ✅ Excelente |
| Threads | OS Threads (~200) | Event Loop (pocos) | Virtual (millones) | Virtual (millones) |
| Base de Datos | Hibernate ORM | Hibernate Reactive | Hibernate ORM | Hibernate ORM |
| Estilo | Imperativo | Reactivo | Imperativo | Imperativo |
| Java Version | 8+ | 8+ | 21+ | 21+ |
| Curva Aprendizaje | Baja | Alta | Media | Media |
Cuándo Usar Cada Enfoque
🟦 Blocking
✅ Aplicaciones pequeñas con bajo tráfico
✅ Equipos sin experiencia en programación reactiva
✅ Prototipos rápidos
❌ No usar en aplicaciones de alto tráfico
⚡ Reactive
✅ Aplicaciones de alto rendimiento
✅ Equipos con experiencia en programación reactiva
✅ Cuando necesitas control fino sobre el flujo asíncrono
❌ No usar si el equipo no está familiarizado con el estilo reactivo
🧵 Virtual Threads
✅ Recomendado para la mayoría de casos
✅ Migración fácil desde código bloqueante
✅ Aplicaciones de alto tráfico con código simple
✅ Mejor balance entre simplicidad y escalabilidad
🎯 Structured Concurrency
✅ Cuando necesitas garantías sobre el ciclo de vida de tareas
✅ Operaciones complejas con múltiples subtareas
✅ Cuando quieres evitar "thread leaks"
✅ Aplicaciones críticas donde la seguridad es importante
🎓 Conceptos Clave
1. Thread.sleep(3000)
Simula una operación lenta (preparar café). En producción sería una llamada a API externa, consulta a base de datos, etc.
2. Hibernate ORM vs Hibernate Reactive
Hibernate ORM (Bloqueante):
Bloquea el thread mientras espera la respuesta de la base de datos
Usado en: Blocking, Virtual, Structured
Hibernate Reactive (No Bloqueante):
No bloquea el thread, usa callbacks
Usado en: Reactive
3. Virtual Threads
Threads ligeros gestionados por la JVM:
Millones de virtual threads pueden ejecutarse sobre pocos threads de plataforma
Cuando un virtual thread se bloquea, el thread de plataforma se libera para otro virtual thread
Introducido en Java 21 (Project Loom)
4. Structured Concurrency
Paradigma que garantiza que:
Las tareas concurrentes tienen un ciclo de vida bien definido
Todas las tareas hijas terminan antes de que el scope padre termine
Evita "thread leaks" y facilita el manejo de errores
5. ManagedExecutor vs ExecutorService
ManagedExecutor:
Gestionado por Quarkus
Usa threads de plataforma
Usado en: Blocking
ExecutorService con VirtualThreads:
Puede usar virtual threads
Más flexible
Usado en: Virtual, Structured
🚀 Cómo Probar los Ejercicios
1. Requisitos Previos
JDK 21+ (o JDK 26 early access según el README)
Docker o Podman corriendo (para la base de datos)
Maven instalado
2. Ejecutar la Aplicación
./mvnw quarkus:dev
La aplicación estará disponible en: http://localhost:8080
3. Probar los Endpoints
Usa el archivo examples.http que contiene todas las requests listas para probar:
### GET Blocking Simple
GET http://localhost:8080/beverage/blocking
### GET Reactive Parallel
GET http://localhost:8080/beverage/reactive/parallel
### GET Virtual Parallel
GET http://localhost:8080/beverage/virtual/parallel
### GET Structured Simple
GET http://localhost:8080/beverage/structured/simple
4. Observar los Logs
Cada endpoint registra información útil:
Qué tipo de bebida se está preparando
Qué threads se están usando
Tiempos de ejecución
5. Comparar Rendimiento
Prueba hacer múltiples requests simultáneas y observa:
Blocking: Se satura rápidamente
Reactive: Maneja muchas requests sin problemas
Virtual: Maneja muchas requests sin problemas
Structured: Maneja muchas requests sin problemas
📊 Ejemplo de Prueba
Escenario: 100 Requests Simultáneas
Blocking: ❌ Muchas requests fallan o timeout
Reactive: ✅ Todas las requests completan exitosamente
Virtual: ✅ Todas las requests completan exitosamente
Structured: ✅ Todas las requests completan exitosamente
Tiempos de Respuesta (3 bebidas paralelas)
Blocking Parallel: ~3 segundos
Reactive Parallel: ~3 segundos
Virtual Parallel: ~3 segundos
Structured Simple: ~3 segundos
🔗 Recursos Adicionales
📝 Notas Finales
Este proyecto es educativo y está diseñado para:
Comparar diferentes enfoques de concurrencia
Entender las ventajas y desventajas de cada uno
Aprender cuándo usar cada enfoque
No está diseñado para producción sin las modificaciones y consideraciones apropiadas.
❓ Preguntas Frecuentes
¿Cuál enfoque debería usar en mi proyecto?
Recomendación: Virtual Threads (módulo 3) para la mayoría de casos, ya que ofrece:
Código simple
Excelente escalabilidad
Fácil migración desde código bloqueante
¿Puedo mezclar enfoques?
Sí, pero no es recomendado. Es mejor elegir un enfoque consistente para toda la aplicación.
¿Virtual Threads reemplazan a Reactive?
No necesariamente. Cada uno tiene su lugar:
Virtual Threads: Para código bloqueante que necesita escalar
Reactive: Para aplicaciones que necesitan control fino sobre el flujo asíncrono
¿Qué pasa si uso Blocking en producción?
Puede funcionar para aplicaciones pequeñas, pero se saturará con alto tráfico. Mejor migrar a Virtual Threads.
¡Disfruta aprendiendo sobre concurrencia en Quarkus! 🎉




