Skip to main content

Command Palette

Search for a command to run...

Concurrencia en Quarkus

Updated
11 min read

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

EndpointDescripciónTiempo Estimado
GET /beverage/blockingUna bebida~3 segundos
GET /beverage/blocking/sequentialTres bebidas secuencialmente~9 segundos
GET /beverage/blocking/parallelTres 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

EndpointDescripciónTiempo Estimado
GET /beverage/reactiveUna bebida reactiva~3 segundos
GET /beverage/reactive/sequentialTres bebidas secuencialmente~9 segundos
GET /beverage/reactive/parallelTres 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 @RunOnVirtualThread para ejecutar en virtual threads

  • Código bloqueante (Thread.sleep) pero sin bloquear threads de plataforma

  • Hibernate ORM bloqueante, pero sobre virtual threads

  • Virtual threads son ligeros (millones posibles)

Endpoints Disponibles

EndpointDescripciónTiempo Estimado
GET /beverage/virtualUna bebida con virtual thread~3 segundos
GET /beverage/virtual/sequentialTres bebidas secuencialmente~9 segundos
GET /beverage/virtual/parallelTres bebidas en paralelo~3 segundos
GET /beverage/virtual/customTres 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 StructuredTaskScope

  • Anotació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 StructuredTaskScope para gestionar tareas concurrentes

  • Garantiza 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

EndpointDescripciónTiempo Estimado
GET /beverage/structured/simpleTres bebidas con StructuredTaskScope básico~3 segundos
GET /beverage/structured/customTres 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ísticaBlockingReactiveVirtualStructured
CódigoSimpleComplejoSimpleSimple
Escalabilidad❌ Limitada✅ Excelente✅ Excelente✅ Excelente
ThreadsOS Threads (~200)Event Loop (pocos)Virtual (millones)Virtual (millones)
Base de DatosHibernate ORMHibernate ReactiveHibernate ORMHibernate ORM
EstiloImperativoReactivoImperativoImperativo
Java Version8+8+21+21+
Curva AprendizajeBajaAltaMediaMedia

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! 🎉

More from this blog

JoeDayz

53 posts

Community Guy | Java Champion | AWS Architect | Software Architect