1. Come creare thread virtuali nella pratica
È ora di passare dalla teoria alla pratica! Sapete già come si crea un thread nel modo classico:
Thread t = new Thread(() -> System.out.println("Hello from thread!"));
t.start();
Oppure in modo ancora più conciso:
new Thread(() -> System.out.println("Hi!")).start();
Ora con Java 21 abbiamo un nuovo modo:
Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));
oppure in modo più esplicito:
Thread t = Thread.ofVirtual().start(() -> System.out.println("Hello from virtual thread!"));
Qual è la differenza?
- Thread.ofVirtual().start(...) crea un thread virtuale (Virtual Thread), gestito dalla JVM e non dal sistema operativo.
- Thread.ofPlatform().start(...) (oppure new Thread(...)) — un thread classico, come prima.
Perché è importante?
I thread virtuali si possono creare a decine di migliaia senza temere OutOfMemoryError. Ora, se vi viene voglia di gestire un milione di richieste — Java dirà: “Nessun problema, avanti ancora!”
2. Sintassi per creare un thread virtuale
Esempio di base:
public class VirtualThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.ofVirtual().start(() -> {
System.out.println("Ciao dal thread virtuale! Thread: " + Thread.currentThread());
});
// Attendiamo la terminazione del thread (per evitare che main finisca prima)
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Cosa succede?
- Creiamo un thread virtuale tramite Thread.ofVirtual().start(...).
- All’interno del thread eseguiamo un’azione semplice: stampare un messaggio.
- Alla fine chiamiamo thread.join() affinché il thread principale attenda la fine di quello virtuale (altrimenti il programma potrebbe terminare prima che il thread stampi qualcosa).
Attenzione:
Il thread virtuale appare e si comporta quasi come uno normale, ma sotto il cofano c’è la “magia” della JVM!
3. Creazione massiva di thread virtuali: la potenza di Loom in pratica
E ora proviamo ciò che con i thread tradizionali sarebbe rischioso (o semplicemente impossibile): creiamo 10_000 thread virtuali, ciascuno dei quali stamperà il proprio numero.
public class VirtualThreadMassive {
public static void main(String[] args) throws InterruptedException {
int N = 10_000;
Thread[] threads = new Thread[N];
for (int i = 0; i < N; i++) {
int threadNum = i;
threads[i] = Thread.ofVirtual().start(() -> {
System.out.println("Thread virtuale #" + threadNum + " sta lavorando!");
});
}
// Attendiamo la terminazione di tutti i thread
for (Thread t : threads) {
t.join();
}
System.out.println("Tutti i thread virtuali sono terminati!");
}
}
- Per i thread tradizionali (new Thread(...)) un tale codice quasi sicuramente “manderebbe in crash” l’app con OutOfMemoryError.
- Per i thread virtuali — è la modalità normale! La JVM gestirà senza problemi migliaia e decine di migliaia di thread.
A proposito, se 10_000 vi sembra tanto, provate 100_000 o persino 1_000_000. Su una macchina moderna la JVM ce la farà, se i vostri thread svolgono lavoro semplice o sono in attesa di I/O.
4. Runnable e lambda: come passare il codice a un thread virtuale
I thread virtuali accettano i task come quelli normali: tramite l’interfaccia Runnable. Ciò significa che potete passare lambda, riferimenti a metodo e qualsiasi oggetto che implementi Runnable.
Esempio con lambda:
Thread.ofVirtual().start(() -> System.out.println("Lambda nel thread virtuale!"));
Esempio con metodo:
public class TaskRunner {
public static void main(String[] args) {
Thread.ofVirtual().start(TaskRunner::doWork);
}
static void doWork() {
System.out.println("Lavoriamo in un thread virtuale: " + Thread.currentThread());
}
}
Esempio con classe anonima:
Thread.ofVirtual().start(new Runnable() {
@Override
public void run() {
System.out.println("Classe anonima in un thread virtuale!");
}
});
Conclusione:
Tutto ciò che funzionava con i thread tradizionali funziona anche con quelli virtuali — solo che ora è “leggero e veloce”.
5. Confronto con ExecutorService: approccio classico e nuovo
ExecutorService classico
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("Task #" + taskNum + " in esecuzione");
});
}
executor.shutdown();
Problema:
Se i task sono troppi e i thread pochi — i task aspettano in coda. Se i thread sono troppi — l’app “annaspa” per mancanza di risorse.
Nuovo approccio: un Executor con thread virtuali
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100_000; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("Task virtuale #" + taskNum);
});
}
executor.shutdown();
Cosa succede?
- Per ogni task viene creato un thread virtuale dedicato.
- La JVM ne gestisce la pianificazione senza sovraccaricare il sistema.
- Non serve limitare la dimensione del pool — i thread virtuali sono “quasi gratuiti”.
Quando usare un Executor con thread virtuali?
- Quando avete un grande flusso di task e non volete preoccuparvi della dimensione del pool.
- Quando i task sono indipendenti e possono essere eseguiti in parallelo.
- Quando volete integrare i thread virtuali in un’architettura esistente che già usa ExecutorService (ad esempio in un server web, un job handler, ecc.).
6. Consigli pratici: quando usare cosa
Quando usare direttamente Thread.ofVirtual().start()?
- Quando serve creare un thread separato per un task unico (ad esempio per un test, una demo o un semplice esperimento).
- Quando il numero di thread è ridotto e volete gestirli manualmente.
Quando usare Executors.newVirtualThreadPerTaskExecutor()?
- Quando dovete lanciare in massa i task (ad esempio elaborazione di molte richieste, file, connessioni di rete).
- Quando i task sono indipendenti e non richiedono coordinamento tra loro.
- Quando volete integrare i thread virtuali in un’architettura esistente in cui è già usato ExecutorService (ad esempio in un server web, un job processor, ecc.).
Suggerimento:
Se non siete sicuri — iniziate con un Executor basato su thread virtuali. È il modo più universale e moderno.
7. Gestione delle eccezioni nei thread virtuali
I thread virtuali sono thread normali dal punto di vista di try-catch. Se all’interno del vostro Runnable si verifica un’eccezione, non “manderà in crash” l’intera JVM, ma terminerà semplicemente quel thread con errore.
Esempio:
Thread t = Thread.ofVirtual().start(() -> {
throw new RuntimeException("Qualcosa è andato storto!");
});
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Il thread principale ha proseguito l'esecuzione.");
In ExecutorService:
Se inviate un task tramite submit, potete ottenere il risultato con Future, e l’eccezione verrà rilanciata alla chiamata di get():
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> f = executor.submit(() -> {
throw new RuntimeException("Errore nel task virtuale");
});
try {
f.get();
} catch (ExecutionException e) {
System.out.println("Abbiamo catturato un'eccezione dal thread virtuale: " + e.getCause());
}
executor.shutdown();
8. Errori tipici nella creazione di thread virtuali
Errore n. 1: Confondere thread virtuali e thread di piattaforma. Se create i thread con new Thread(...) o Thread.ofPlatform(), non sono thread virtuali. Solo Thread.ofVirtual().start(...) o i metodi di Executors forniscono veri Virtual Threads.
Errore n. 2: Aspettarsi un’accelerazione per calcoli pesanti. I thread virtuali non accelerano i task che gravano molto sulla CPU (CPU-bound). Se avete un milione di thread, ognuno dei quali calcola il pi greco fino al milionesimo decimale, la JVM non può “accelerare” i calcoli: farà solo più switching tra i thread.
Errore n. 3: Allocare risorse (ad esempio connessioni al database) per ogni thread. Se create un milione di thread virtuali ma ognuno richiede una connessione dedicata al database — il DB non reggerà. I thread virtuali sono ideali per task in cui la maggior parte del tempo è attesa (I/O) e non lavoro con risorse esterne limitate.
Errore n. 4: Non attendere la fine dei thread quando è importante. Se il thread principale termina prima dei thread virtuali — il programma può chiudersi senza attendere i risultati. Usate join() oppure ExecutorService con shutdown() e awaitTermination().
Errore n. 5: Usare librerie obsolete non compatibili con i thread virtuali. Alcune librerie di terze parti possono bloccare i thread a livello di sistema operativo o usare sincronizzazione nativa, riducendo l’efficacia dei thread virtuali. Verificate sempre la compatibilità.
GO TO FULL VERSION