CodeGym /Corsi /JAVA 25 SELF /Scoped Values e nuove meccaniche dei thread (Java 21+)

Scoped Values e nuove meccaniche dei thread (Java 21+)

JAVA 25 SELF
Livello 57 , Lezione 4
Disponibile

1. Perché ThreadLocal sta perdendo rilevanza

A cosa serve davvero ThreadLocal?

Nella multithreading classica, dove i thread vivono a lungo (per esempio su un server), a volte è necessario mantenere dati propri per ogni thread — dati che non devono incrociarsi con quelli degli altri. Per esempio, il nome utente, l’ID della richiesta o un buffer temporaneo.

Per farlo, in Java è nato ThreadLocal<T> — una sorta di “spazio personale” del thread, dove si possono conservare dati senza intralciare i vicini:

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

user.set("Alice"); // il valore è conservato solo per questo thread
String name = user.get(); // qui restituirà "Alice"; negli altri thread — null

Perché ThreadLocal non si sposa con i thread virtuali

I thread virtuali vivono in modo del tutto diverso rispetto ai vecchi thread “pesanti”. Nascono e scompaiono a migliaia — a volte in frazioni di millisecondo. E ThreadLocal lega i dati a un thread specifico come se dovesse vivere per sempre.

Quando un thread virtuale termina il lavoro, i suoi dati in ThreadLocal possono rimanere appesi in memoria — anche se il thread stesso è già morto da tempo. Questo porta a leak, perché la JVM non sempre sa che quei valori non servono più a nessuno.

E se i thread vengono riutilizzati (per esempio nei pool), è possibile una situazione ancora più spiacevole: un contesto “estraneo” può passare accidentalmente a una nuova richiesta. Immagina che l’utente Petya riceva i dati di Vasya — ed ecco bug e vulnerabilità.

ThreadLocal si trova benissimo dove i thread sono pochi e longevi. Ma con i thread virtuali — è come provare a conservare oggetti in un armadio che scompare ogni secondo.

2. Scoped Values: un nuovo modo di propagare il contesto

Scoped Values è uno strumento recente di Java 21 che risolve il vecchio problema di ThreadLocal, ma in modo elegante. Invece di conservare i dati dentro il thread, come in ThreadLocal, li “aggancia” all’ambito di esecuzione — cioè a un tratto concreto di codice. Il valore vive solo finché viene eseguita quella porzione, poi scompare automaticamente, senza lasciare tracce in memoria.

import java.lang.ScopedValue;

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

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

Quando il codice esce dai limiti del blocco run, il valore non è più disponibile — tentare di accedervi genererà un’eccezione. Non serve ripulire nulla manualmente.

Gli Scoped Values non sporcano la memoria, non confondono il contesto tra thread e consentono di creare ambiti annidati, in cui i valori interni coprono temporaneamente quelli esterni. È un modo ordinato, prevedibile e sicuro per propagare il contesto, soprattutto nel mondo dei thread virtuali.

3. Esempi d’uso di Scoped Values

Esempio 1: Propagazione del contesto utente

Supponiamo di avere un server che elabora richieste di utenti diversi. Per ogni richiesta vogliamo sapere chi l’ha iniziata.

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("Elaboriamo per l'utente: " + USER.get());
    }
}

Cosa succederà:

  • Per ogni richiesta viene creato il proprio scope in cui USER è uguale a “Alice” o “Bob”.
  • Dentro handleBusinessLogic() otteniamo sempre il nome utente corretto.
  • Non appena l’elaborazione della richiesta termina, il valore scompare.

Esempio 2: Logging con contesto

Supponiamo di voler inserire automaticamente l’identificatore della richiesta nei log:

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("Inizio elaborazione");
                doWork();
                log("Fine elaborazione");
            });
        }
    }

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

    static void doWork() {
        log("Stiamo lavorando...");
    }
}

Risultato (esempio):

[REQ-1] Inizio elaborazione
[REQ-1] Stiamo lavorando...
[REQ-1] Fine elaborazione
[REQ-2] Inizio elaborazione
[REQ-2] Stiamo lavorando...
[REQ-2] Fine elaborazione
[REQ-3] Inizio elaborazione
[REQ-3] Stiamo lavorando...
[REQ-3] Fine elaborazione

Ogni scope conserva il proprio identificatore di richiesta, e non è possibile alcuna confusione tra thread.

4. Scoped Values e thread virtuali: una coppia perfetta

Perché gli Scoped Values sono particolarmente utili con i thread virtuali

I thread virtuali vivono poco — sono creati e distrutti a migliaia, a volte in frazioni di secondo. Pertanto, l’approccio tradizionale con ThreadLocal, dove i dati sono saldamente “legati” al thread stesso, qui semplicemente non funziona: i thread scompaiono troppo in fretta e il contesto può accidentalmente trapelare o confondersi.

ScopedValue, al contrario, lega i dati al task stesso — al suo ambito di esecuzione. Ciò significa che il contesto (per esempio, il nome utente o l’ID della richiesta) segue il codice, non il thread. Quando il task termina, il valore scompare automaticamente. Per i thread virtuali è la soluzione ideale: sicura, pulita e senza sorprese.

Esempio: Elaborazione massiva di task con thread virtuali

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() {
        // Per ogni task il proprio TASK_ID
        System.out.println("Elaborazione del task #" + TASK_ID.get());
    }
}

Punti chiave:

  • Per ogni task viene creato il proprio scope del valore TASK_ID.
  • Anche se i task vengono eseguiti in parallelo, i valori non si confondono tra thread.
  • Nessun memory leak: lo scope “muore” insieme al task.

5. Confronto: ThreadLocal vs ScopedValue

Criterio ThreadLocal ScopedValue
Associazione Al thread All'ambito di codice (scope)
Ciclo di vita Finché il thread è vivo Finché è in esecuzione lo scope
Sicurezza Rischio di leak e confusione Nessun leak, nessuna confusione
Thread virtuali Inefficiente, rischioso Ideale
Utilizzo
set/get
where(...).run(...), get
Annidamento Non supporta override Consente di sovrascrivere temporaneamente i valori

6. Ambiti annidati (scope): shadowing dei valori

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

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

Risultato:

Esterno
Interno
Esterno

È comodo se, per esempio, all’interno di un task occorre ridefinire temporaneamente il valore di contesto.

Scoped Values: scenari tipici di utilizzo

  • Propagazione dell’identificatore utente o della richiesta: per loggare azioni o verificare i permessi.
  • Logging: inserimento automatico del contesto nei log.
  • Tracing: per debug e profilazione.
  • Parametri di transazione: ad esempio livello di isolamento o modalità di funzionamento.
  • Qualsiasi “contesto” visibile solo entro i limiti di un singolo task (o delle sue sotto-attività).

7. Altre meccaniche nuove: Structured Concurrency

Structured Concurrency è un approccio in cui i task correlati (per esempio, sottoprocessi di una stessa operazione) vengono gestiti come un tutt’uno: se il task padre termina o fallisce, tutti i task figli vengono automaticamente annullati. Questo riduce il rischio di thread “dimenticati” o “appesi”.

Esempio (molto schematico):

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

    scope.join(); // aspettiamo il completamento di entrambe
    scope.throwIfFailed(); // se anche solo una è fallita — rilanciamo un'eccezione

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

Vantaggi:

  • Gestione del ciclo di vita dei task più pulita.
  • Nessun sottoprocesso “appeso”.
  • Gestione degli errori più semplice.

Structured Concurrency è attualmente in modalità preview, ma è già in forte sviluppo.

8. Consigli pratici e limitazioni

Quando usare gli Scoped Values?

  • Sempre quando occorre propagare contesto tra task, soprattutto con thread virtuali.
  • Se prima utilizzavi ThreadLocal, valuta se non sia meglio passare a ScopedValue.

Quando serve ancora ThreadLocal?

  • In rari casi, quando il thread vive molto a lungo e il contesto deve essere “permanente” per tutta la sua vita (per esempio lavorando con codice legacy).

Limitazioni

  • Gli Scoped Values non possono essere modificati dopo la creazione dello scope — sono “sola lettura”.
  • Gli Scoped Values non possono essere usati fuori dallo scope: provare a ottenere il valore al di fuori dell’ambito genererà un’eccezione.
  • Non usare gli Scoped Values per conservare oggetti grandi — l’ambito dovrebbe essere leggero e veloce.

9. Errori tipici nell’uso di Scoped Values

Errore n. 1: tentativo di ottenere il valore fuori dallo scope. Se chiami USER.get() fuori dal blocco ScopedValue.where(...), otterrai l’eccezione NoSuchElementException. Verifica che l’accesso avvenga solo all’interno dell’ambito.

Errore n. 2: tentativo di modificare il valore dentro lo scope. Gli Scoped Values non sono contenitori mutabili. Se devi “ridefinire” temporaneamente un valore, crea uno scope annidato.

Errore n. 3: uso congiunto di ThreadLocal e ScopedValue. Evita di mescolare queste meccaniche senza una reale necessità — può portare a confusione e a errori di contesto.

Errore n. 4: hai dimenticato d’incapsulare la logica nel blocco run(). Se hai scritto ScopedValue.where(USER, "Alice") senza .run(() -> { ... }), non verrà creato alcuno scope!

Errore n. 5: tentativo di usare Scoped Value per informazioni globali longeve. Per tali scopi è meglio usare variabili normali o ThreadLocal (se giustificato).

1
Sondaggio/quiz
Thread virtuali, livello 57, lezione 4
Non disponibile
Thread virtuali
Thread virtuali
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION