CodeGym /Cours /JAVA 25 SELF /Scoped Values et nouvelles mécaniques de threads (Java 21...

Scoped Values et nouvelles mécaniques de threads (Java 21+)

JAVA 25 SELF
Niveau 57 , Leçon 4
Disponible

1. Pourquoi ThreadLocal perd de sa pertinence

À quoi sert ThreadLocal au juste?

Dans la concurrence classique, où les threads vivent longtemps (par exemple sur un serveur), il faut parfois conserver des données propres à chaque thread — des données qui ne doivent pas se croiser avec celles des autres. Par exemple, le nom d’utilisateur, l’ID de requête ou un tampon temporaire.

Pour cela, Java a introduit ThreadLocal<T> — une sorte d’« espace personnel » du thread, où l’on peut stocker des données sans gêner les voisins:

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

user.set("Alice"); // la valeur est conservée uniquement pour ce thread
String name = user.get(); // renverra "Alice" ici même, dans les autres threads — null

Pourquoi ThreadLocal s’entend mal avec les threads virtuels

Les threads virtuels vivent tout autrement que les anciens threads « lourds ». Ils apparaissent et disparaissent par milliers — parfois en une fraction de milliseconde. Et ThreadLocal attache les données à un thread donné, comme s’il allait vivre éternellement.

Lorsque un thread virtuel termine, ses données dans ThreadLocal peuvent rester suspendues en mémoire — même si le thread lui-même est déjà mort. Cela conduit à des fuites, car la JVM ne sait pas toujours que ces valeurs ne sont plus utilisées.

Et si les threads sont réutilisés (par exemple dans des pools), une situation encore plus désagréable est possible: un « contexte étranger » peut accidentellement passer à une nouvelle requête. Imaginez que l’utilisateur Peter reçoive les données de Basil — bonjour les bugs et les vulnérabilités.

ThreadLocal se porte très bien là où il y a peu de threads et où ils vivent longtemps. Mais avec les threads virtuels — c’est comme essayer de stocker ses affaires dans une armoire qui disparaît chaque seconde.

2. Scoped Values: une nouvelle façon de transmettre le contexte

Scoped Values est un outil récent de Java 21 qui résout l’ancien problème de ThreadLocal, mais de façon élégante. Au lieu de stocker les données à l’intérieur du thread, comme avec ThreadLocal, il les « attache » à la portée d’exécution — c’est-à-dire à un segment précis de code. La valeur ne vit que pendant l’exécution de ce segment, puis disparaît automatiquement sans laisser de traces en mémoire.

import java.lang.ScopedValue;

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

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

Quand le code sort du bloc run, la valeur n’est déjà plus accessible — toute tentative d’y accéder provoquera une exception. Aucun nettoyage manuel n’est nécessaire.

Les Scoped Values n’encombrent pas la mémoire, ne mélangent pas les contextes entre threads et permettent de créer des portées imbriquées où les valeurs internes masquent temporairement les externes. C’est un moyen soigné, prévisible et sûr de transmettre le contexte, surtout dans un monde de threads virtuels.

3. Exemples d’utilisation de Scoped Values

Exemple 1: propagation du contexte utilisateur

Supposons que nous ayons un serveur qui traite des requêtes de différents utilisateurs. Pour chaque requête, nous voulons savoir qui l’a initiée.

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("Traitement pour l'utilisateur : " + USER.get());
    }
}

Ce qui se passera:

  • Pour chaque requête, un scope est créé, dans lequel USER vaut « Alice » ou « Bob ».
  • À l’intérieur de handleBusinessLogic(), nous obtenons toujours le nom d’utilisateur correct.
  • Dès que le traitement de la requête est terminé, la valeur disparaît.

Exemple 2: journalisation avec contexte

Supposons que nous voulions insérer automatiquement l’identifiant de requête dans les 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("Début du traitement");
                doWork();
                log("Fin du traitement");
            });
        }
    }

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

    static void doWork() {
        log("Traitement en cours...");
    }
}

Résultat (exemple):

[REQ-1] Début du traitement
[REQ-1] Traitement en cours...
[REQ-1] Fin du traitement
[REQ-2] Début du traitement
[REQ-2] Traitement en cours...
[REQ-2] Fin du traitement
[REQ-3] Début du traitement
[REQ-3] Traitement en cours...
[REQ-3] Fin du traitement

Chaque scope conserve son identifiant de requête, et aucune confusion entre threads n’est possible.

4. Scoped Values et threads virtuels: la paire idéale

Pourquoi les Scoped Values sont particulièrement utiles avec les threads virtuels

Les threads virtuels vivent peu de temps — ils sont créés et détruits par milliers, parfois en une fraction de seconde. Par conséquent, l’ancienne approche avec ThreadLocal, où les données sont « attachées » strictement au thread lui-même, ne fonctionne tout simplement pas ici: les threads disparaissent trop vite, et le contexte peut accidentellement fuir ou se mélanger.

ScopedValue, au contraire, attache les données à la tâche elle-même — à sa portée d’exécution. Cela signifie que le contexte (par exemple, le nom de l’utilisateur ou l’ID de requête) suit le code, et non le thread. Lorsque la tâche se termine, la valeur disparaît automatiquement. Pour les threads virtuels, c’est la solution idéale: sûre, propre et sans surprises.

Exemple: traitement massif de tâches avec des threads virtuels

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() {
        // Chaque tâche a son propre TASK_ID
        System.out.println("Traitement de la tâche #" + TASK_ID.get());
    }
}

Points clés:

  • Pour chaque tâche, un scope de valeur TASK_ID est créé.
  • Même si les tâches s’exécutent en parallèle, les valeurs ne se mélangent pas entre les threads.
  • Pas de fuite mémoire: le scope « meurt » en même temps que la tâche.

5. Comparaison: ThreadLocal vs ScopedValue

Critère ThreadLocal ScopedValue
Association Au thread À la portée de code (scope)
Cycle de vie Tant que le thread vit Pendant l’exécution du scope
Sécurité Risque de fuites, confusion Pas de fuites, pas de confusion
Threads virtuels Inefficace, dangereux Idéal
Utilisation
set/get
where(...).run(...), get
Imbrication Ne prend pas en charge la redéfinition Permet de masquer les valeurs

6. Portées imbriquées (scopes): masquage des valeurs

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

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

Résultat:

Externe
Interne
Externe

C’est pratique, par exemple, lorsqu’il faut redéfinir temporairement la valeur du contexte à l’intérieur d’une même tâche.

Scoped Values: scénarios d’utilisation typiques

  • Propagation de l’identifiant d’utilisateur ou de requête: pour journaliser les actions ou vérifier les droits.
  • Journalisation: insertion automatique du contexte dans les logs.
  • Traçage: pour le debug et le profilage.
  • Paramètres de transaction: par exemple, le niveau d’isolation ou le mode de fonctionnement.
  • Tout « contexte » visible uniquement dans les limites d’une tâche (ou de ses sous-tâches).

7. Autres nouvelles mécaniques: Structured Concurrency

Structured Concurrency est une approche selon laquelle des tâches liées (par exemple, des sous-processus d’une même opération) sont gérées comme un tout: si la tâche parente se termine ou échoue, toutes les tâches enfants sont automatiquement annulées. Cela réduit le risque de threads « oubliés » ou « pendants ».

Exemple (très schématique):

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

    scope.join(); // on attend la fin des deux
    scope.throwIfFailed(); // si l'une a échoué — on lève une exception

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

Avantages:

  • Gestion plus propre du cycle de vie des tâches.
  • Pas de sous-processus « pendants ».
  • Gestion des erreurs facilitée.

Structured Concurrency est encore en mode preview, mais évolue déjà activement.

8. Conseils pratiques et limites

Quand utiliser Scoped Values?

  • Chaque fois qu’il faut transmettre du contexte entre des tâches, surtout avec des threads virtuels.
  • Si vous utilisiez auparavant ThreadLocal — demandez-vous s’il ne vaut pas mieux passer à ScopedValue.

Quand a-t-on encore besoin de ThreadLocal?

  • Dans de rares cas, quand un thread vit très longtemps et que le contexte doit rester « permanent » pendant toute sa durée de vie (par exemple, avec du code legacy).

Limites

  • On ne peut pas modifier les Scoped Values après la création du scope — elles sont en lecture seule.
  • On ne peut pas utiliser les Scoped Values en dehors d’un scope: tenter d’obtenir la valeur hors de la portée déclenchera une exception.
  • N’utilisez pas les Scoped Values pour stocker de gros objets — la portée doit rester légère et rapide.

9. Erreurs courantes avec Scoped Values

Erreur n° 1: tentative d’obtenir la valeur en dehors du scope. Si vous appelez USER.get() en dehors du bloc ScopedValue.where(...), vous obtiendrez une exception NoSuchElementException. Vérifiez que l’accès n’a lieu qu’à l’intérieur de la portée.

Erreur n° 2: tentative de modifier la valeur dans un scope. Les Scoped Values ne sont pas un conteneur mutable. Si vous devez « redéfinir » temporairement une valeur, créez un scope imbriqué.

Erreur n° 3: utiliser ThreadLocal et ScopedValue ensemble. Évitez de mélanger ces mécaniques sans nécessité absolue — cela peut conduire à de la confusion et à des erreurs de contexte.

Erreur n° 4: vous avez oublié d’englober la logique dans le bloc run(). Si vous avez écrit ScopedValue.where(USER, "Alice") sans .run(() -> { ... }), aucun scope ne sera créé!

Erreur n° 5: tentative d’utiliser Scoped Value pour des informations globales de longue durée. Pour de tels besoins, il vaut mieux utiliser des variables ordinaires ou ThreadLocal (si c’est justifié).

1
Mission
JAVA 25 SELF, niveau 57, leçon 4
Bloqué
Confidentialité des données dans un réseau d'entreprise 🔒
Confidentialité des données dans un réseau d'entreprise 🔒
1
Mission
JAVA 25 SELF, niveau 57, leçon 4
Bloqué
Suivi des commandes pour un service de livraison par drones 📦
Suivi des commandes pour un service de livraison par drones 📦
1
Étude/Quiz
Virtual Threads, niveau 57, leçon 4
Indisponible
Virtual Threads
Virtual Threads
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION