CodeGym /Corsi /JAVA 25 SELF /Creazione di thread virtuali: Thread.ofVirtual().start()

Creazione di thread virtuali: Thread.ofVirtual().start()

JAVA 25 SELF
Livello 57, Lezione 1
Disponibile

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à.

1
Compito
JAVA 25 SELF, livello 57, lezione 1
Bloccato
Telemetria della vettura da corsa 🏎️
Telemetria della vettura da corsa 🏎️
1
Compito
JAVA 25 SELF, livello 57, lezione 1
Bloccato
Gestione dell'assistente robot sperimentale 🤖
Gestione dell'assistente robot sperimentale 🤖
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION