CodeGym /Cursos /JAVA 25 SELF /Scoped Values y nuevas mecánicas de hilos (Java 21+)

Scoped Values y nuevas mecánicas de hilos (Java 21+)

JAVA 25 SELF
Nivel 57 , Lección 4
Disponible

1. Por qué ThreadLocal pierde relevancia

¿Para qué sirve ThreadLocal?

En la concurrencia clásica, donde los hilos viven mucho tiempo (por ejemplo, en un servidor), a veces necesitamos almacenar datos propios para cada hilo, de modo que no se crucen con los de otros. Por ejemplo, el nombre de usuario, el ID de la solicitud o un búfer temporal.

Para esto apareció en Java ThreadLocal<T>: una especie de «espacio personal» del hilo, donde puedes guardar datos sin molestar a los demás:

ThreadLocal<String> user = new ThreadLocal<>();

user.set("Alice"); // el valor se guarda solo para este hilo
String name = user.get(); // devolverá "Alice" aquí; en otros hilos — null

Por qué ThreadLocal no se lleva bien con los hilos virtuales

Los hilos virtuales viven de forma muy distinta a los «pesados» de antes. Aparecen y desaparecen por miles — a veces en fracciones de milisegundo. Y ThreadLocal vincula los datos a un hilo concreto como si fuese a vivir para siempre.

Cuando un hilo virtual termina su trabajo, sus datos en ThreadLocal pueden quedarse colgando en memoria, incluso aunque el hilo ya haya muerto. Esto provoca fugas, porque la JVM no siempre sabe que esos valores ya no los necesita nadie.

Y si los hilos se reutilizan (por ejemplo, en pools), puede darse una situación aún más desagradable: un contexto «ajeno» puede pasar accidentalmente a una nueva solicitud. Imagina que el usuario Petya recibe los datos de Vasya — y listo: bugs y vulnerabilidades.

ThreadLocal funciona muy bien cuando hay pocos hilos y viven mucho tiempo. Pero con hilos virtuales es como intentar guardar cosas en un armario que desaparece cada segundo.

2. Scoped Values: una nueva forma de propagar contexto

Scoped Values es una herramienta reciente de Java 21 que resuelve el viejo problema de ThreadLocal, pero de forma elegante. En lugar de guardar los datos dentro del hilo, como hace ThreadLocal, los «adhiere» al ámbito de ejecución, es decir, a un fragmento concreto de código. El valor vive solo mientras se ejecuta ese fragmento y luego desaparece automáticamente, sin dejar residuos en memoria.

import java.lang.ScopedValue;

ScopedValue<String> USER = ScopedValue.newInstance();

ScopedValue.where(USER, "Alice").run(() -> {
    System.out.println("Hello, " + USER.get()); // Mostrará: Hello, Alice
});

Cuando el código sale del bloque run, el valor ya no está disponible: intentar acceder a él lanzará una excepción. No hace falta limpiar nada manualmente.

Scoped Values no ensucia la memoria, no confunde el contexto entre hilos y permite crear ámbitos anidados, donde los valores internos temporalmente superponen a los externos. Es una forma ordenada, predecible y segura de propagar contexto, especialmente en el mundo de los hilos virtuales.

3. Ejemplos de uso de Scoped Values

Ejemplo 1: Propagación del contexto de usuario

Supongamos que tenemos un servidor que procesa solicitudes de distintos usuarios. Para cada solicitud queremos saber quién la inició.

import java.lang.ScopedValue;

public class ServerExample {
    static final ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) {
        processRequest("Alice");
        processRequest("Bob");
    }

    static void processRequest(String userName) {
        ScopedValue.where(USER, userName).run(() -> {
            handleBusinessLogic();
        });
    }

    static void handleBusinessLogic() {
        System.out.println("Procesando para el usuario: " + USER.get());
    }
}

Qué ocurrirá:

  • Para cada solicitud se crea su propio scope, en el que USER vale «Alice» o «Bob».
  • Dentro de handleBusinessLogic() siempre obtenemos el nombre de usuario correcto.
  • En cuanto termina el procesamiento de la solicitud, el valor desaparece.

Ejemplo 2: Registro (logging) con contexto

Supongamos que queremos insertar automáticamente el identificador de solicitud en los logs:

import java.lang.ScopedValue;

public class LoggingExample {
    static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            String reqId = "REQ-" + i;
            ScopedValue.where(REQUEST_ID, reqId).run(() -> {
                log("Inicio del procesamiento");
                doWork();
                log("Fin del procesamiento");
            });
        }
    }

    static void log(String message) {
        System.out.printf("[%s] %s%n", REQUEST_ID.get(), message);
    }

    static void doWork() {
        log("Trabajando...");
    }
}

Resultado (ejemplo):

[REQ-1] Inicio del procesamiento
[REQ-1] Trabajando...
[REQ-1] Fin del procesamiento
[REQ-2] Inicio del procesamiento
[REQ-2] Trabajando...
[REQ-2] Fin del procesamiento
[REQ-3] Inicio del procesamiento
[REQ-3] Trabajando...
[REQ-3] Fin del procesamiento

Cada scope guarda su propio identificador de solicitud y no hay posibilidad de confusión entre hilos.

4. Scoped Values y hilos virtuales: la pareja ideal

Por qué Scoped Values es especialmente útil con hilos virtuales

Los hilos virtuales viven poco tiempo — se crean y destruyen por miles, a veces en fracciones de segundo. Por eso el enfoque antiguo con ThreadLocal, donde los datos están «anclados» al propio hilo, aquí simplemente no funciona: los hilos desaparecen demasiado rápido y el contexto puede fugarse o mezclarse por accidente.

ScopedValue, en cambio, vincula los datos a la tarea en sí — a su ámbito de ejecución. Esto significa que el contexto (por ejemplo, el nombre de usuario o el ID de la solicitud) sigue al código y no al hilo. Cuando la tarea termina, el valor desaparece automáticamente. Para hilos virtuales es la solución ideal: segura, limpia y sin sorpresas.

Ejemplo: procesamiento masivo de tareas con hilos virtuales

import java.lang.ScopedValue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadScopedValueDemo {
    static final ScopedValue<Integer> TASK_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 1; i <= 10_000; i++) {
            int taskId = i;
            executor.submit(() -> ScopedValue.where(TASK_ID, taskId).run(() -> {
                processTask();
            }));
        }

        executor.shutdown();
    }

    static void processTask() {
        // Para cada tarea, su propio TASK_ID
        System.out.println("Procesando la tarea #" + TASK_ID.get());
    }
}

Puntos clave:

  • Para cada tarea se crea su propio scope del valor TASK_ID.
  • Aunque las tareas se ejecuten en paralelo, los valores no se confunden entre hilos.
  • No hay fugas de memoria: el scope «muere» junto con la tarea.

5. Comparación: ThreadLocal vs ScopedValue

Criterio ThreadLocal ScopedValue
Vinculación Al hilo Al ámbito de código (scope)
Ciclo de vida Mientras viva el hilo Mientras se ejecute el scope
Seguridad Riesgo de fugas y confusión Sin fugas, sin confusión
Hilos virtuales Ineficaz, peligroso Ideal
Uso
set/get
where(...).run(...), get
Anidamiento No admite override Se pueden superponer valores

6. Ámbitos anidados (scopes): superposición de valores

ScopedValue<String> INFO = ScopedValue.newInstance();

ScopedValue.where(INFO, "Externo").run(() -> {
    System.out.println(INFO.get()); // "Externo"
    ScopedValue.where(INFO, "Interno").run(() -> {
        System.out.println(INFO.get()); // "Interno"
    });
    System.out.println(INFO.get()); // "Externo"
});

Resultado:

Externo
Interno
Externo

Esto es útil si, por ejemplo, dentro de una tarea necesitas redefinir temporalmente el valor del contexto.

Scoped Values: escenarios de uso típicos

  • Propagación del identificador de usuario o de solicitud: para registrar acciones o verificar permisos.
  • Registro (logging): inserción automática de contexto en los logs.
  • Trazas: para depuración y perfilado.
  • Parámetros de transacciones: por ejemplo, nivel de aislamiento o modo de trabajo.
  • Cualquier «contexto» visible solo dentro de una tarea (o sus subtareas).

7. Otras mecánicas nuevas: Structured Concurrency

Structured Concurrency es un enfoque en el que las tareas relacionadas (por ejemplo, subprocesos de una operación) se gestionan como un todo: si la tarea padre finaliza o falla, todas las tareas hijas se cancelan automáticamente. Esto reduce el riesgo de hilos «olvidados» o «colgados».

Ejemplo (muy esquemático):

try (var scope = StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> result1 = scope.fork(() -> fetchData1());
    Future<String> result2 = scope.fork(() -> fetchData2());

    scope.join(); // esperamos a que finalicen ambas
    scope.throwIfFailed(); // si cualquiera falla — lanzamos una excepción

    String combined = result1.resultNow() + result2.resultNow();
    System.out.println(combined);
}

Ventajas:

  • Gestión más limpia del ciclo de vida de las tareas.
  • No hay subprocesos «colgados».
  • Más fácil manejar errores.

Structured Concurrency está actualmente en modo preliminar (preview), pero evoluciona activamente.

8. Consejos prácticos y limitaciones

¿Cuándo usar Scoped Values?

  • Siempre que necesites propagar contexto entre tareas, especialmente con hilos virtuales.
  • Si antes usabas ThreadLocal, plantéate si no es mejor pasarte a ScopedValue.

¿Cuándo sigue siendo necesario ThreadLocal?

  • En casos raros, cuando un hilo vive mucho y el contexto debe ser «permanente» durante toda su vida (por ejemplo, al trabajar con código heredado).

Limitaciones

  • No se pueden modificar los Scoped Values tras crear el scope: son de solo lectura.
  • No se pueden usar Scoped Values fuera del scope: intentar obtener el valor fuera del ámbito lanzará una excepción.
  • No uses Scoped Values para almacenar objetos grandes: el ámbito debe ser ligero y rápido.

9. Errores típicos al usar Scoped Values

Error n.º 1: intento de obtener el valor fuera del scope. Si llamas a USER.get() fuera del bloque ScopedValue.where(...), obtendrás la excepción NoSuchElementException. Verifica que el acceso ocurra solo dentro del ámbito.

Error n.º 2: intentar cambiar el valor dentro del scope. Scoped Values no es un contenedor mutable. Si necesitas «redefinir» temporalmente el valor, crea un scope anidado.

Error n.º 3: usar ThreadLocal y ScopedValue a la vez. No conviene mezclar estas mecánicas salvo causa mayor: puede provocar confusión y errores de contexto.

Error n.º 4: olvidaste envolver la lógica en el bloque run(). Si escribes ScopedValue.where(USER, "Alice") sin .run(() -> { ... }), ¡no se creará ningún scope!

Error n.º 5: intentar usar Scoped Value para información global de larga vida. Para esos casos es mejor usar variables normales o ThreadLocal (si está justificado).

1
Tarea
JAVA 25 SELF, nivel 57, lección 4
Bloqueada
Confidencialidad de datos en la red corporativa 🔒
Confidencialidad de datos en la red corporativa 🔒
1
Tarea
JAVA 25 SELF, nivel 57, lección 4
Bloqueada
Seguimiento de pedidos en un servicio de entrega con drones 📦
Seguimiento de pedidos en un servicio de entrega con drones 📦
1
Cuestionario/control
Virtual Threads, nivel 57, lección 4
No disponible
Virtual Threads
Virtual Threads
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION