Se sviluppi in Java da qualche anno, ti sarai sicuramente imbattuto nei ThreadLocal. Che sia per memorizzare il contesto di sicurezza di un utente, una transazione di database o una sessione HTTP, i ThreadLocal sono stati per vent’anni lo standard di fatto per salvare dati legati al singolo thread di esecuzione.
Con l’avvento di Java 25 (la nuova versione LTS) e la definitiva maturazione dei Virtual Threads, i ThreadLocal mostrano però tutti i loro limiti architetturali.
Fortunatamente, la JEP 506 introduce la soluzione definitiva: gli Scoped Values. Scopriamo perché cambieranno il tuo modo di scrivere codice e come usarli da oggi.
Il “lato oscuro” dei ThreadLocal
Per capire il valore degli Scoped Values, dobbiamo prima ricordare i tre grandi problemi strutturali dei ThreadLocal:
- Mutabilità incontrollata: Qualsiasi metodo all’interno dello stesso thread può invocare
.set()e modificare il dato. Questo rende il flusso del codice imprevedibile e difficile da debuggare. - Memory Leak dietro l’angolo: Un
ThreadLocalmantiene i dati finché il thread è vivo o finché non si chiama esplicitamente.remove(). Se usi un Thread Pool (come in Tomcat o Quarkus), dimenticare un.remove()significa che i dati della richiesta dell’Utente A rimarranno in memoria e potrebbero essere letti accidentalmente dalla richiesta dell’Utente B. - L’impatto devastante sui Virtual Threads: Con i Virtual Threads possiamo lanciare milioni di thread contemporaneamente. Se ognuno di essi eredita la mappa dei
ThreadLocaldal thread genitore (tramiteInheritableThreadLocal), la memoria heap collassa in pochi secondi a causa delle continue copie dei dati.
La svolta di Java 25: Cosa sono gli Scoped Values?
Gli Scoped Values (introdotti dalla JEP 506) nascono per risolvere questi problemi alla radice. Sono contenitori di dati progettati per essere:
- Immutabili: Una volta associato un valore, questo può essere solo letto, mai modificato.
- A tempo limitato (Scoped): Il valore esiste solo all’interno di un perimetro di codice ben definito (un blocco o una lambda). Una volta usciti da quel perimetro, il dato scompare da solo.
- Leggeri: La condivisione dei dati con i thread figli (compresi i Virtual Threads) non copia nulla in memoria, ma riutilizza la struttura esistente con un overhead quasi nullo.
Scoped Values in Azione: Un esempio pratico
Vediamo come cambia il codice. Immaginiamo di voler passare il nome dell’utente autenticato attraverso i vari livelli della nostra applicazione (Controller -> Service -> Repository) senza doverlo passare come parametro in ogni singolo metodo.
java
import java.lang.ScopedValue;
public class Application {
// 1. Dichiarazione dello ScopedValue (tipicamente static e final)
private static final ScopedValue<String> CONTESTO_UTENTE = ScopedValue.newInstance();
public static void main(String[] args) {
String utenteArrivato = "Alice";
// Prima del blocco, lo ScopedValue è vuoto ("unbound")
System.out.println("Prima: " + CONTESTO_UTENTE.orElse("Anonimo")); // Stampa: Anonimo
// 2. Definiamo lo scope di validità del dato usando il pattern fluent
ScopedValue.where(CONTESTO_UTENTE, utenteArrivato).run(() -> {
// All'interno di questo blocco .run(), il valore è accessibile ovunque
eseguiLogicaDiBusiness();
// Funziona nativamente anche dentro i Virtual Threads figli!
Thread.ofVirtual().start(() -> {
System.out.println("[Virtual Thread] Lettura: " + CONTESTO_UTENTE.get()); // Stampa: Alice
});
});
// 3. Una volta usciti dal blocco .run(), il dato viene rimosso automaticamente
System.out.println("Dopo: " + CONTESTO_UTENTE.orElse("Anonimo")); // Stampa: Anonimo
}
private static void eseguiLogicaDiBusiness() {
// Il metodo legge il valore in sicurezza senza bisogno di riceverlo come parametro
if (CONTESTO_UTENTE.isBound())
System.out.println("Esecuzione operazione per l'utente: " + CONTESTO_UTENTE.get()); // Stampa: Alice
}
}
Usa il codice con cautela.
Perché questo codice è migliore?
Guardando l’esempio balzano subito all’occhio tre vantaggi enormi:
- Niente Memory Leak: Non c’è nessun blocco
try-finallycon all’interno unCONTESTO_UTENTE.remove(). Non serve. Appena la lambda passata a.run()termina, Java pulisce tutto automaticamente. - Sicurezza del dato: Non esiste un metodo
CONTESTO_UTENTE.set(). All’interno dieseguiLogicaDiBusiness(), nessuno sviluppatore potrà sovrascrivere accidentalmente il nome dell’utente. - Integrazione perfetta con la concorrenza moderna: Il Virtual Thread figlio legge il contesto istantaneamente e senza sprecare byte di memoria RAM.
Conclusioni
I ThreadLocal hanno fatto la storia di Java, ma l’era del cloud computing e della programmazione concorrente su larga scala richiede strumenti più robusti e sicuri. Con Java 25, gli Scoped Values si confermano la scelta standard per la gestione dei contesti di esecuzione.
Se stai progettando un nuovo framework o aggiornando i tuoi servizi aziendali a Java 25, è il momento di iniziare la migrazione: il tuo codice sarà più pulito, più sicuro e decisamente più veloce.

