# Día 18: Concurrencia en la JVM: De Hilos Clásicos a Virtual Threads en Java 21

> Project Loom, Platform Threads vs Virtual Threads, Thread Pools y Concurrencia Estructurada

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 18 de 30
- Fase 4: Seguridad, Concurrencia y Reactividad vs Virtual Threads
- Dedicación: 4h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/virtual-threads/

## Al terminar este día sabrás

- Explicar la diferencia de modelo entre hilos de plataforma y Virtual Threads.
- Describir qué ocurre cuando un hilo virtual se bloquea y cuándo no puede desmontarse.
- Activar Virtual Threads en Spring Boot y limitar la concurrencia sin pools.
- Situar Scoped Values y Structured Concurrency y su estado actual.


## Conceptos

### 1. Platform Threads vs Virtual Threads: Modelo 1:1 frente a Modelo M:N

Un Platform Thread está mapeado directamente a un hilo del sistema operativo kernel (1:1). Un Virtual Thread es una abstracción gestionada por la JVM que se monta sobre un pool pequeño de hilos de soporte del sistema operativo llamados **Carrier Threads** (modelo M:N).

**Cómo funciona por dentro.**

Un hilo de plataforma (`Thread` clásico) es una envoltura de un hilo del sistema operativo: relación 1 a 1. Cada uno reserva una pila de alrededor de 1 MB y el cambio de contexto lo hace el kernel. Por eso los servidores usan pools de un par de cientos de hilos: crear miles es caro y lento.

Un hilo virtual (Java 21, JEP 444) es un objeto de la JVM. Muchos hilos virtuales (M) se ejecutan sobre unos pocos hilos portadores (N), que son hilos de plataforma de un `ForkJoinPool` con tantos hilos como núcleos. La pila de un hilo virtual vive en el heap y crece según la necesita, así que ocupa unos cientos de bytes al empezar. Crear un millón es razonable.

Los hilos virtuales no hacen el código más rápido: hacen que esperar sea barato. Brillan en cargas de E/S (servidores web, llamadas a bases de datos y APIs) y no aportan nada en cálculo intensivo de CPU, donde el límite son los núcleos.

*10.000 tareas bloqueantes con hilos virtuales*

```java
// Java 21: un hilo virtual por tarea. El try cierra el executor y espera a que terminen.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));   // simula una llamada de E/S
            return i;
        }));
}
// Tarda ~1 segundo. Con Executors.newFixedThreadPool(200) tardaría ~50 segundos.

// Otras formas de crearlos
Thread t1 = Thread.ofVirtual().name("worker-", 0).start(() -> System.out.println("hola"));
Thread t2 = Thread.startVirtualThread(() -> System.out.println(Thread.currentThread().isVirtual()));
```

- **En una línea:** `Thread.ofVirtual().start(() -> System.out.println("En hilo virtual"));`
- **Por qué importa:** Permite que operaciones bloqueantes de red o base de datos liberen el hilo del SO para otros procesos mientras esperan.
- **Error típico:** Crear un Thread Pool (`ThreadPoolExecutor`) para Virtual Threads; los hilos virtuales son tan baratos que se crean y descartan bajo demanda sin reutilizarlos.
- **Java 17 frente a 21:** Exclusivo de Java 21 LTS en adelante.
- **Se conecta con:** Memoria Stack, Planificador ForkJoinPool, Rendimiento I/O

### 2. Desmontaje Automático en Bloqueos I/O (Unmounting)

Cuando un Virtual Thread ejecuta una operación bloqueante (leer de base de datos con JDBC, hacer una llamada HTTP con HttpClient, o `Thread.sleep()`), la JVM guarda el estado del hilo virtual en el Heap y desmonta el hilo del Carrier Thread del SO.

**Cómo funciona por dentro.**

Cuando un hilo virtual ejecuta una operación bloqueante del JDK (leer de un socket, `Thread.sleep`, esperar un `Lock` o una `BlockingQueue`), la JVM guarda su pila en el heap como una continuación y lo desmonta de su portador. El portador queda libre y toma otro hilo virtual listo para ejecutarse. Cuando la operación termina, el hilo virtual vuelve a la cola y se monta en el primer portador libre, que puede ser otro distinto.

Para que esto funcione, el JDK se reescribió por dentro: las APIs de red y de concurrencia de `java.util.concurrent` saben desmontarse. Tu código sigue siendo síncrono y secuencial, con trazas de pila legibles y depuración normal, pero se comporta como código asíncrono en cuanto a uso de recursos.

Esto es lo que lo diferencia de la programación reactiva: obtienes la escalabilidad sin reescribir la lógica en cadenas de callbacks u operadores.

*Ver el cambio de portador*

```java
Runnable tarea = () -> {
    System.out.println("antes:   " + Thread.currentThread());
    try {
        Thread.sleep(100);                  // punto de desmontaje
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    System.out.println("después: " + Thread.currentThread());
};

try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 4; i++) ex.submit(tarea);
}

// antes:   VirtualThread[#23]/runnable@ForkJoinPool-1-worker-1
// después: VirtualThread[#23]/runnable@ForkJoinPool-1-worker-3   <- otro portador
```

- **En una línea:** `try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> consultarBD()); }`
- **Por qué importa:** El Carrier Thread queda inmediatamente libre para ejecutar otro hilo virtual. Cuando la respuesta de red llega, la JVM vuelve a montar el hilo virtual en cualquier Carrier Thread libre.
- **Error típico:** Creer que Virtual Threads aceleran operaciones intensivas de CPU; solo escalan operaciones limitadas por I/O (red, disco, base de datos).
- **Java 17 frente a 21:** Revolución introducida en Java 21.
- **Se conecta con:** Sockets no bloqueantes, JDBC sincrónico, Throughput masivo

### 3. El Peligro del Pinning: synchronized vs ReentrantLock

Si un Virtual Thread entra en un bloque `synchronized` y realiza una operación de I/O bloqueante dentro de él, el hilo virtual queda "anclado" (pinned) a su Carrier Thread del sistema operativo, impidiendo que la JVM lo desmonte.

**Cómo funciona por dentro.**

Un hilo virtual queda «clavado» (pinned) a su portador cuando no puede desmontarse durante un bloqueo. En Java 21 ocurre en dos casos: mientras está dentro de un bloque o método `synchronized`, y mientras ejecuta código nativo (JNI) o una llamada de vuelta desde él. Si en esa situación se bloquea en E/S, el portador también se bloquea.

Con pocos portadores (uno por núcleo), unos cuantos hilos clavados a la vez pueden dejar sin portadores al resto de la aplicación: el rendimiento se desploma o parece un bloqueo. Por eso se recomendaba sustituir `synchronized` alrededor de operaciones de E/S por `ReentrantLock`, que sí permite desmontar. Muchas librerías (drivers JDBC, clientes HTTP) se actualizaron por este motivo.

Java 24 (JEP 491) eliminó el pinning por `synchronized`, pero en Java 21, la LTS que usarás en producción, sigue existiendo. Se detecta con el evento JFR `jdk.VirtualThreadPinned` o con `-Djdk.tracePinnedThreads=full`.

*Evitar el pinning en Java 21*

```java
// Problema en Java 21: E/S dentro de synchronized clava el hilo virtual
class ClienteLegado {
    synchronized String consultar(String url) throws IOException {
        return http.send(peticion(url), BodyHandlers.ofString()).body();  // portador bloqueado
    }
}

// Solución: ReentrantLock permite desmontar mientras espera
class Cliente {
    private final ReentrantLock lock = new ReentrantLock();

    String consultar(String url) throws IOException, InterruptedException {
        lock.lock();
        try {
            return http.send(peticion(url), BodyHandlers.ofString()).body();
        } finally {
            lock.unlock();
        }
    }
}

// Diagnóstico
// java -Djdk.tracePinnedThreads=full -jar app.jar         (imprime la pila al clavarse)
// jcmd <pid> JFR.start name=pin settings=profile           (evento jdk.VirtualThreadPinned)
```

- **En una línea:** `private final ReentrantLock lock = new ReentrantLock(); lock.lock(); try { ... } finally { lock.unlock(); }`
- **Por qué importa:** El pinning prolongado puede agotar los Carrier Threads y degradar el rendimiento a niveles peores que los hilos clásicos.
- **Error típico:** Hacer llamadas remotas o queries SQL dentro de métodos con la palabra clave `synchronized`.
- **Java 17 frente a 21:** En Java 21 se diagnostica con la flag `-Djdk.tracePinnedThreads=full`. Se soluciona reemplazando `synchronized` por `ReentrantLock`.
- **Se conecta con:** ReentrantLock, Diagnóstico JVM, Thread Dumps

### 4. Activación de Virtual Threads en Spring Boot 3.2+

En Spring Boot 3.2+ corriendo sobre Java 21, basta con una única propiedad en `application.properties`: `spring.threads.virtual.enabled=true`.

**Cómo funciona por dentro.**

En Spring Boot 3.2+ con Java 21, `spring.threads.virtual.enabled=true` cambia de golpe todos los ejecutores que Boot autoconfigura: Tomcat o Jetty atienden cada petición en un hilo virtual, y `@Async`, `@Scheduled` y los listeners de Kafka y RabbitMQ también los usan.

El cambio de mentalidad es importante: antes, el tamaño del pool de hilos limitaba de forma implícita cuántas peticiones llegaban a la vez a la base de datos o a un servicio externo. Con hilos virtuales ese freno desaparece, y la presión se traslada a los recursos de verdad limitados. Si necesitas limitar la concurrencia hacia un recurso, no crees un pool de hilos virtuales: usa un `Semaphore`, o un bulkhead de Resilience4j.

Comprueba en tus logs o con `Thread.currentThread().isVirtual()` que realmente se están usando, y vigila el uso de `ThreadLocal` costosos (cachés por hilo), que con millones de hilos dejan de ser una buena idea.

*Activar y limitar la concurrencia*

```java
// application.yml
// spring:
//   threads:
//     virtual:
//       enabled: true

@Service
class CatalogoExterno {
    private final RestClient cliente;
    private final Semaphore maxConcurrentes = new Semaphore(20);   // el proveedor admite 20 a la vez

    CatalogoExterno(RestClient.Builder builder) {
        this.cliente = builder.baseUrl("https://catalogo.proveedor.com").build();
    }

    public Producto obtener(String sku) throws InterruptedException {
        maxConcurrentes.acquire();                 // el hilo virtual espera barato, sin bloquear portador
        try {
            return cliente.get().uri("/productos/{sku}", sku).retrieve().body(Producto.class);
        } finally {
            maxConcurrentes.release();
        }
    }
}

@GetMapping("/debug/hilo")
String hilo() { return Thread.currentThread().toString(); }   // VirtualThread[#81]/runnable@...
```

- **En una línea:** `spring.threads.virtual.enabled=true`
- **Por qué importa:** Automáticamente, Tomcat embebido pasa a atender cada petición HTTP entrante con un Virtual Thread nuevo, y los ejecutores de `@Async` y `@Scheduled` usan ejecutores de hilos virtuales sin modificar ni una sola línea de código Java.
- **Error típico:** Activar la propiedad en un servidor que corre sobre Java 17; la propiedad será ignorada o provocará un error de arranque.
- **Java 17 frente a 21:** Solo posible con Java 21 y Spring Boot 3.2+.
- **Se conecta con:** Tomcat Embebido, Spring Boot 3.2, Zero-Code Migration

### 5. Scoped Values y Concurrencia Estructurada (Preview / Futuro)

En Java 21, `ThreadLocal` sigue funcionando en Virtual Threads, pero tener millones de hilos con ThreadLocals pesados puede saturar el Heap. Los `ScopedValue` permiten compartir datos inmutables y de corta duración entre hilos de forma más ligera y segura.

**Cómo funciona por dentro.**

Dos APIs complementan a los hilos virtuales y en Java 21 están en preview (necesitan `--enable-preview`). **Scoped Values** (JEP 446) es una alternativa a `ThreadLocal` para pasar datos de contexto (usuario, id de traza) a lo largo de una llamada: el valor es inmutable, solo existe dentro de un ámbito delimitado y se hereda de forma barata a los subprocesos. Se publicó como definitiva en Java 25.

**Structured Concurrency** (JEP 453) trata un grupo de tareas concurrentes como una unidad: si lanzas tres consultas en paralelo dentro de un `StructuredTaskScope`, el ámbito no se cierra hasta que terminan todas, y si una falla puedes cancelar las demás automáticamente. Evita hilos huérfanos y fugas. Su API ha cambiado entre versiones y sigue en preview.

En producción con Java 21 lo prudente es conocerlas y no depender de ellas. Mientras tanto, `CompletableFuture` o un executor de hilos virtuales cubren los casos de paralelismo de E/S.

*Structured Concurrency (preview en Java 21)*

```java
// Compilar y ejecutar con --enable-preview --release 21
record Panel(Usuario usuario, List<Tarea> tareas, Estadisticas stats) {}

Panel cargarPanel(long usuarioId) throws InterruptedException, ExecutionException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Subtask<Usuario> usuario = scope.fork(() -> usuarios.buscar(usuarioId));
        Subtask<List<Tarea>> tareas = scope.fork(() -> tareasRepo.deUsuario(usuarioId));
        Subtask<Estadisticas> stats = scope.fork(() -> analitica.de(usuarioId));

        scope.join()            // espera a las tres...
             .throwIfFailed();  // ...y si una falló, cancela el resto y propaga el error

        return new Panel(usuario.get(), tareas.get(), stats.get());
    }   // al salir del bloque no queda ningún hilo vivo
}

// Equivalente estable en Java 21 sin preview
try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
    var u = ex.submit(() -> usuarios.buscar(usuarioId));
    var t = ex.submit(() -> tareasRepo.deUsuario(usuarioId));
    return new Panel(u.get(), t.get(), analitica.de(usuarioId));
}
```

- **En una línea:** `ScopedValue.where(USUARIO_ACTUAL, usuario).run(() -> procesarOrden());`
- **Por qué importa:** Prepara el camino para la concurrencia estructurada, donde tareas padre coordinan el ciclo de vida de múltiples tareas hijas concurrentes.
- **Error típico:** Almacenar objetos gigantescos en `ThreadLocal` cuando se usan millones de hilos virtuales.
- **Java 17 frente a 21:** Preview en 21, evolución en curso en 22 y 23.
- **Se conecta con:** ThreadLocal, Concurrencia Estructurada, Garbage Collector


## Sesión de 90 minutos

### Bloque 1: Comparativa Práctica Platform vs Virtual (0 - 25 min)

- **Qué estudiar:** Ejecución de un bucle de 10.000 tareas con `Executors.newVirtualThreadPerTaskExecutor()` vs Platform Threads.
- **Pregunta a responder:** ¿Cuánto tiempo y memoria consume lanzar 10.000 hilos de cada tipo?
- **Entregable:** Script Java 21 que demuestre que 10.000 Virtual Threads terminan en segundos sin agotar memoria.

### Bloque 2: Detección y Mitigación de Pinning (25 - 50 min)

- **Qué estudiar:** Uso de la propiedad `-Djdk.tracePinnedThreads=full` y refactorización de `synchronized` a `ReentrantLock`.
- **Pregunta a responder:** ¿Cómo saber si una librería de terceros ancla tus Carrier Threads?
- **Entregable:** Código de ejemplo refactorizado con `ReentrantLock` probado con la flag de pinning.

### Bloque 3: Integración en Spring Boot 3.2+ (50 - 80 min)

- **Qué estudiar:** Configuración de `spring.threads.virtual.enabled=true` y verificación del nombre del hilo en logs (`VirtualThread[#...]/runnable@ForkJoinPool-1-worker-X`).
- **Pregunta a responder:** ¿Cómo verificar que los controladores REST atienden peticiones en hilos virtuales?
- **Entregable:** Endpoint REST que imprima `Thread.currentThread().isVirtual()` devolviendo `true`.

### Cierre y Consolidación (80 - 90 min)

- **Qué estudiar:** Repaso de las 5 reglas de oro de concurrencia en Java 21.
- **Pregunta a responder:** ¿Cuándo NO usar Virtual Threads?
- **Entregable:** Resumen en notas personales con el balance de pros/contras.


## Explícalo desde cero

### Por qué Virtual Threads elimina la necesidad de librerías reactivas para I/O simple

- **Qué entender:** La programación reactiva (WebFlux/Reactor) nació para resolver un problema de los Platform Threads: no bloquear hilos del sistema operativo. Pero obligaba a escribir código en cadenas complejas (`mono.flatMap(...)`) difícil de depurar. Con Virtual Threads, puedes volver a escribir código simple secuencial (`var res = httpClient.send(...)`) y la JVM se encarga de no bloquear el hilo del SO por debajo.
- **Qué suele explicarse mal:** Decir que los Virtual Threads hacen que un hilo individual vaya "más rápido". No aceleran la latencia individual; multiplican el throughput (capacidad de peticiones concurrentes simultáneas).
- **Ejemplo sencillo:** Platform Threads son como 100 camiones pesados en una autopista. Virtual Threads son como 100.000 bicicletas ligeras que van por el mismo carril: si una bicicleta frena a esperar, no traba a las demás.
- **Señal de alerta:** Si propones poner Virtual Threads en una aplicación de cálculo numérico o minería de datos intensiva en CPU esperando que vaya 10 veces más rápido.


## Preguntas

### Nivel 1 (Básico): ¿Por qué NUNCA se debe utilizar un `Executors.newFixedThreadPool(200)` para gestionar Virtual Threads?

Evalúa la comprensión del ciclo de vida de los hilos virtuales. Los thread pools existen para amortizar el altísimo coste de creación de un Platform Thread del sistema operativo (~1MB de stack, syscall del kernel). Un Virtual Thread cuesta apenas unos cientos de bytes en Heap y se crea en microsegundos; agruparlos en un pool limita artificialmente su escalabilidad.

**Respuesta razonada:** Porque los pools existen para reutilizar recursos caros, y un hilo virtual no lo es: crearlo cuesta casi lo mismo que crear un objeto. Un pool fijo de 200 hilos virtuales reintroduce justo el límite artificial que los hilos virtuales vienen a eliminar: la tarea 201 espera aunque haya recursos de sobra, y además pierdes la semántica de «un hilo por tarea», que facilita depurar y propagar contexto.

Si lo que buscas con ese 200 es limitar cuántas operaciones llegan a la vez a un recurso (una base de datos, un proveedor externo), la herramienta correcta es un `Semaphore` con 200 permisos alrededor de esa llamada, combinado con `newVirtualThreadPerTaskExecutor()`.

### Nivel 2 (Intermedio): ¿Qué es el "Thread Pinning" en Java 21 y por qué los drivers JDBC antiguos causaban problemas con Virtual Threads?

El pinning ocurre cuando un hilo virtual no puede desmontarse de su Carrier Thread debido a que está dentro de un bloque `synchronized` nativo o frame de JNI. Muchos drivers JDBC antiguos utilizaban `synchronized` en cada operación de socket, secuestrando los Carrier Threads de la máquina.

**Respuesta razonada:** El pinning ocurre cuando un hilo virtual se bloquea mientras está dentro de un bloque `synchronized` o de código nativo: la JVM de Java 21 no puede desmontarlo y el hilo portador queda bloqueado con él. Como solo hay tantos portadores como núcleos, unos pocos hilos clavados pueden paralizar toda la aplicación.

Los drivers JDBC antiguos protegían el acceso al socket de la conexión con `synchronized`. Cada consulta clavaba su hilo virtual durante toda la espera a la base de datos, así que con 8 núcleos solo había 8 consultas avanzando a la vez y el resto de hilos virtuales se quedaba sin portador. Los fabricantes cambiaron `synchronized` por `ReentrantLock` (el driver de PostgreSQL lo hizo en la versión 42.6) y Java 24 eliminó el problema de raíz.

### Nivel 3 (Entrevista Senior): Si una aplicación con Virtual Threads hace 50.000 peticiones simultáneas a una base de datos PostgreSQL, ¿por qué el Connection Pool de HikariCP puede convertirse en el cuello de botella?

Evalúa la visión holística de arquitectura. Virtual Threads permite que tu servidor Java maneje 50.000 peticiones concurrentes sin inmutarse, pero tu base de datos relacional (PostgreSQL/MySQL) no puede manejar 50.000 conexiones concurrentes sin colapsar. HikariCP limitará las conexiones a su tamaño máximo (e.g. 50 conexiones), por lo que 49.950 hilos virtuales quedarán esperando un recurso de base de datos. Se debe dimensionar el pool y proteger la BD con semáforos o colas.

**Respuesta razonada:** Porque los hilos virtuales eliminan el límite de hilos, pero no el de conexiones. Las 50.000 peticiones llegan a la vez al pool de HikariCP, que tiene, digamos, 20 conexiones. Veinte consultas avanzan y 49.980 hilos virtuales quedan esperando una conexión libre. Si no la consiguen en `connection-timeout`, fallan con `SQLTransientConnectionException`.

Aumentar el pool no lo arregla: PostgreSQL tiene un límite de conexiones (`max_connections`, 100 por defecto) y cada una consume memoria en el servidor; con cientos de consultas a la vez rinde peor. Las soluciones son limitar la concurrencia de entrada (semáforo, bulkhead, rate limiting) para devolver errores rápidos o encolar, reducir el trabajo por petición (caché, consultas más eficientes, lotes) y, si de verdad hace falta, escalar la base de datos o añadir réplicas de lectura.


## Reto del tutor

**Reto:** Un equipo propone migrar su aplicación de Java 17 a Java 21 y activar `spring.threads.virtual.enabled=true`. El líder de infraestructura pregunta: "¿Esto significa que podemos quitar los límites de memoria RAM de nuestros pods en Kubernetes?". Responde como un ingeniero senior de rendimiento.

- **Lo que está bien:** Explica que los Virtual Threads no eliminan el consumo de memoria y que el heap puede crecer si hay demasiadas peticiones vivas al mismo tiempo.
- **Respuesta superficial:** Decir que sí porque "los hilos virtuales no pesan nada".
- **Lo que falta de fondo:** No entender que cada petición concurrente viva retiene objetos en el Heap (DTOs, resultados de queries, buffers de red). Si antes 200 hilos retenían 200 peticiones simultáneas, ahora 20.000 hilos virtuales activos retendrán 20.000 peticiones vivas en el Heap, disparando la presión sobre el Garbage Collector y pudiendo provocar un OutOfMemoryError si no hay límites de concurrencia.
- **Respuesta senior:** Bajo ningún concepto se deben quitar los límites de memoria; de hecho, se deben monitorizar con mayor rigor. Aunque cada hilo virtual consume casi cero stack, la carga concurrente en el Heap aumenta proporcionalmente al número de peticiones simultáneas en vuelo (cada petición tiene su árbol de objetos en memoria esperando respuestas de base de datos o APIs externas). Además, se debe verificar que las librerías de terceros no causen Thread Pinning y que el pool de base de datos (HikariCP) esté calibrado para no saturar el servidor de PostgreSQL. Virtual Threads democratiza la concurrencia masiva, pero traslada la presión de los hilos del SO al Garbage Collector y a los sistemas downstream.


## Katas

### Kata 18.1: Lanzamiento de 50.000 Tareas Concurrentes con Virtual Threads

Escribe un programa en Java 21 con `try (var executor = Executors.newVirtualThreadPerTaskExecutor())` que lance 50.000 tareas que simulen una llamada I/O de 100ms con `Thread.sleep(100)` y verifica que todas finalizan en menos de 1 segundo.

### Kata 18.2: Refactorización Anti-Pinning con ReentrantLock

Toma una clase con un método `synchronized` que realice una simulación de lectura de socket, ejecuta la app con `-Djdk.tracePinnedThreads=full`, observa la alerta de pinning en consola y refactorízala usando `java.util.concurrent.locks.ReentrantLock` comprobando que el warning desaparece.


## Reglas de oro

- Nunca uses Thread Pools con Virtual Threads; utiliza siempre `Executors.newVirtualThreadPerTaskExecutor()`.
- Los Virtual Threads están diseñados para tareas limitadas por I/O (red, disco, base de datos), NO para cálculo masivo intensivo en CPU.
- Reemplaza bloques `synchronized` por `ReentrantLock` en secciones críticas que realicen operaciones I/O bloqueantes.
- En Spring Boot 3.2+, activar `spring.threads.virtual.enabled=true` transforma automáticamente Tomcat a hilos virtuales.
- Controla siempre la concurrencia hacia recursos downstream limitados (bases de datos relacionales) mediante Semaphore o dimensionamiento adecuado de HikariCP.


## Java 17 frente a Java 21

| Característica | Java 17 | Java 21 |
| --- | --- | --- |
| Tamaño en Memoria | ~1 MB por Platform Thread (Stack SO) | < 1 KB por Virtual Thread (en Heap) |
| Límite Típico Concurrencia | ~1.000 a 3.000 hilos antes de saturar SO | Millones de hilos concurrentes |
| Bloqueo I/O | Bloquea el hilo del sistema operativo | Desmonta el hilo virtual (Carrier Thread queda libre) |
| Spring Boot 3.2+ Config | No soportado | spring.threads.virtual.enabled=true |


## Proyecto guiado «Pedidos»: Virtual threads en el servicio

Atender más peticiones concurrentes sin cambiar el estilo del código.

**Tareas:**

- Activa `spring.threads.virtual.enabled=true` con Java 21.
- Busca bloques `synchronized` en la reserva de stock y sustitúyelos por `ReentrantLock` si hacen I/O dentro.
- Mide con una carga sencilla (por ejemplo, 500 peticiones concurrentes) la latencia antes y después.

**Está listo cuando:**

- Tienes anotada la latencia p95 con y sin virtual threads.
- Con `-Djdk.tracePinnedThreads=full` no aparecen avisos de pinning en tu código.


## Fuentes oficiales

- [JEP 444: Virtual Threads](https://openjdk.org/jeps/444)
- [Java 21: guía de hilos virtuales](https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html)

