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 | |
|
| 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).
GO TO FULL VERSION