CodeGym /Corsi /JAVA 25 SELF /Thread contro thread virtuali: differenze e vantaggi

Thread contro thread virtuali: differenze e vantaggi

JAVA 25 SELF
Livello 57 , Lezione 0
Disponibile

1. Thread classici: come funziona e dove fa male

Ricordiamo prima come funzionano i thread normali in Java (detti anche di piattaforma o nativi). Quelli creati tramite new Thread(...).

Quando chiamate new Thread(() -> { ... }).start();, la JVM non lancia semplicemente un pezzo di codice. Chiede al sistema operativo di creare un vero thread di esecuzione. Il sistema operativo gli assegna uno stack separato (di solito alcuni megabyte) e riserva altre risorse di servizio.

Questo thread vive finché il suo compito è in esecuzione e per tutto questo tempo occupa un posto nella tabella dei thread del sistema operativo. Più thread di questo tipo ci sono, più memoria va ai loro stack e maggiore è il carico sul sistema operativo. Ecco perché, con un gran numero di thread in esecuzione simultaneamente, l’applicazione può iniziare a «soffocare» — il sistema spende troppo tempo a commutare tra di loro.

Esempio: thread classico

Thread thread = new Thread(() -> {
    System.out.println("Ciao dal thread!");
});
thread.start();

Sembra semplice, vero? Ma provate a crearne non uno o due, bensì, diciamo, diecimila — e il vostro programma inizierà rapidamente a soffocare. Finirà la memoria oppure il sistema dirà che è stato raggiunto il limite di thread. Non è un bug di Java, ma una conseguenza naturale dell’architettura: i thread sono oggetti pesanti e costosi.

Perché succede? Ogni thread riceve il proprio stack (di solito 1–2 megabyte), più un intero set di strutture di servizio dal sistema operativo. Inoltre, il sistema operativo non è felice quando gli si presentano decine di migliaia di thread — ha dei limiti, spesso piuttosto rigidi.

E anche se la memoria non finisce, nasce un altro problema — la commutazione di contesto. Quando i thread sono troppi, il sistema salta continuamente tra di loro, salvando e ripristinando il loro stato. Tutto ciò richiede tempo e riduce le prestazioni, così il «massiccio parallelismo» non porta il guadagno atteso.

Il problema «un thread — una richiesta»

Nelle vecchie applicazioni server (per esempio su Tomcat o Jetty) si usava spesso il modello «thread‑per‑request»: per ogni richiesta in arrivo si assegnava un thread separato. È comodo, ma se avete 10_000 utenti, vi servono 10_000 thread! Il server fatica, e inizia la corsa alla memoria, non alla velocità di elaborazione.

Risultato:
I thread classici sono ottimi per un numero ridotto di attività parallele, ma non si scalano a decine o centinaia di migliaia.

2. Cosa sono i thread virtuali (Virtual Threads)?

Qui entrano in gioco i thread virtuali. Non sono semplicemente «un altro thread», ma un’idea architetturale completamente diversa.

I thread virtuali sono thread gestiti non dal sistema operativo, ma dalla stessa JVM. Sono implementati interamente dentro Java e possono essere creati in quantità enormi (decine e centinaia di migliaia) senza «gonfiare» la memoria e senza rallentamenti.

In breve:

  • Platform Thread (thread di piattaforma): un thread normale che corrisponde direttamente a un thread del sistema operativo.
  • Virtual Thread (thread virtuale): un thread leggero gestito dalla JVM, non dal sistema operativo.

Come funziona?

I thread virtuali sono thread «leggeri» che vivono non nel sistema operativo, ma dentro la JVM. Lavorano sopra un piccolo pool di thread reali, chiamati platform threads. Si può immaginare che la JVM li gestisca come un direttore d’orchestra: ha un numero limitato di musicisti (thread reali), ma distribuisce abilmente tra loro le parti (thread virtuali).

Schema architetturale:

+-------------------+           +-------------------+
|  Virtual Thread 1 |---\       |  Platform Thread  |
|  Virtual Thread 2 |---->====> |  (Carrier Thread) |
|  Virtual Thread 3 |---/       +-------------------+
         ...                        (Sistema operativo)

Un Carrier Thread è un normale thread del sistema operativo su cui la JVM esegue molti thread virtuali. Se un thread virtuale si blocca all’improvviso — per esempio, attende dati dal disco o dalla rete — la JVM lo «congela», liberando il carrier thread per altri compiti.

Perché è rivoluzionario?

Perché ora si può scrivere codice abituale e lineare — senza callback infiniti, CompletableFuture e catene «infernali» di thenApply — e allo stesso tempo scalare l’applicazione a migliaia di operazioni simultanee.

I thread virtuali occupano solo decine di kilobyte (invece dei megabyte dei thread normali) e si creano praticamente all’istante. Perciò li si può avviare e distruggere a migliaia, senza temere che il sistema operativo crolli sotto il loro peso. Questo rende finalmente la programmazione concorrente in Java facile e naturale.

3. Vantaggi dei thread virtuali

Scalabilità

Con i thread virtuali potete permettervi il lusso di avviare decine e centinaia di migliaia di attività parallele. Per esempio, elaborare ogni richiesta di rete in un thread separato — senza preoccuparvi che il server «scoppi».

Dimostrazione: 100 000 thread virtuali

for (int i = 0; i < 100_000; i++) {
    Thread.ofVirtual().start(() -> {
        // Qui può esserci qualsiasi logica
        try {
            Thread.sleep(1000); // Simulazione di lavoro
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}
System.out.println("Tutti i thread sono stati avviati!");

Questo codice gira tranquillamente su un normale laptop!
Provate a fare lo stesso con i thread classici — e vedrete OutOfMemoryError o trasformerete il computer in un «mattone».

Semplicità di programmazione

I thread virtuali permettono di scrivere il consueto codice «bloccante», senza trasformarlo in «spaghetti» di chiamate asincrone. Per esempio, potete usare tranquillamente Thread.sleep, InputStream.read, Socket.accept — la JVM si occuperà di non bloccare l’intero carrier thread.

Migliore leggibilità e manutenzione del codice

Invece di schemi complessi con callback e CompletableFuture scrivete codice lineare e comprensibile. Questo riduce i bug e facilita la manutenzione.

Niente reinvenzione della ruota

In passato, per elaborare migliaia di richieste in parallelo, bisognava usare framework asincroni, librerie reattive (Netty, Vert.x, Project Reactor), che richiedono uno stile di programmazione particolare. Ora si può farne a meno — e ottenere comunque scalabilità.

4. Architettura: come funzionano i thread virtuali «sotto il cofano»

Mapping sui carrier threads

La JVM crea un piccolo pool di thread reali (carrier threads) — di solito quanti sono i core del processore. Tutti i thread virtuali «viaggiano» su questi carrier threads, come passeggeri sugli autobus.

  • Quando un thread virtuale si blocca (per esempio, attende una risposta dalla rete), la JVM lo «scarica» dal carrier thread e lo mette in coda.
  • Non appena il thread può riprendere, la JVM lo «rimette» su un carrier thread libero.

Analogia:
Immaginate di avere 4 taxi (carrier threads) e di servire 10 000 clienti (virtual threads). Appena un cliente arriva a destinazione e scende, il taxi ne prende subito un altro. Nessuno resta fermo, e i taxi non «si rompono» sotto il peso dei passeggeri.

Pianificazione e commutazione

La JVM decide autonomamente quale thread virtuale eseguire in un dato momento. Se un thread si blocca su I/O, non impedisce agli altri di lavorare.

5. Limitazioni e particolarità dei thread virtuali

Non è tutto oro ciò che è virtuale

Non per calcoli prolungati: Se avete un compito che occupa costantemente la CPU (heavy CPU‑bound), il thread virtuale non darà un incremento di prestazioni. Perché i carrier threads restano comunque limitati dal numero di core.

Alcuni lock non sono efficienti: I vecchi meccanismi di sincronizzazione (per esempio la sincronizzazione con synchronized su oggetti con mutex nativi) possono impedire alla JVM di «congelare» il thread virtuale. In tali casi il carrier thread attenderà insieme al thread virtuale, riducendo la scalabilità.

Non tutte le librerie vanno d’accordo con i thread virtuali: Se una libreria effettua chiamate native o usa lock specifici, i thread virtuali possono comportarsi in modo inatteso.

Esempio: quando non conviene usare i Virtual Threads

Se avete un compito che gira in un ciclo infinito e fa calcoli, il thread virtuale non darà alcun vantaggio. Ci si scontra comunque con il numero di core.

Thread.ofVirtual().start(() -> {
    while (true) {
        // Conteggio all'infinito
    }
});

Risultato:
Un carrier thread sarà occupato da questo thread virtuale, e le altre attività attenderanno il loro turno.

6. Confronto: Platform Thread vs Virtual Thread

Caratteristica Platform Thread (classico) Virtual Thread (virtuale)
Gestito da dal sistema operativo dalla JVM
Memoria per thread Megabyte Decine di kilobyte
Numero di thread Di solito < 10_000 Migliaia, centinaia di migliaia
Costo di creazione Costoso Economico
Scalabilità Limitata Quasi illimitata
Adatto a Compiti lunghi, CPU‑bound Compiti brevi, I/O‑bound
Commutazione dei thread SO JVM
Compatibilità 100% Quasi sempre, ma con alcune sfumature

7. Esempio: come apparirebbe un server prima e dopo i Virtual Threads

Prima (Platform Threads)

ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket client = serverSocket.accept();
    new Thread(() -> handleClient(client)).start();
}

Problema:
Dopo 5 000 connessioni il server inizierà ad «annaspare».

Dopo (Virtual Threads, Java 21+)

ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket client = serverSocket.accept();
    Thread.ofVirtual().start(() -> handleClient(client));
}

Magia:
Ora è possibile gestire decine di migliaia di connessioni — senza pensare ai limiti dei thread!

9. Errori tipici nel passaggio ai thread virtuali

Errore n. 1: aspettarsi un’accelerazione per i compiti computazionali. I thread virtuali non accelerano i compiti che saturano completamente la CPU. Per tali compiti si è comunque limitati dal numero di core.

Errore n. 2: utilizzare vecchie sincronizzazioni bloccanti. Se usate vecchi lock (per esempio synchronized su oggetti che possono essere «bloccati» nativamente), i thread virtuali possono non essere scaricati dal carrier thread, perdendo tutti i vantaggi.

Errore n. 3: ignorare il comportamento delle librerie di terze parti. Alcune librerie di terze parti potrebbero non essere pronte a lavorare con i thread virtuali (per esempio se usano JNI o lock nativi).

Errore n. 4: aspettarsi una crescita magica delle prestazioni. I thread virtuali non sono una panacea. Non accelerano tutto, ma rendono il parallelismo economico e comodo per i compiti I/O‑bound.

1
Compito
JAVA 25 SELF, livello 57, lezione 0
Bloccato
Avvio del messaggero segreto 💬
Avvio del messaggero segreto 💬
1
Compito
JAVA 25 SELF, livello 57, lezione 0
Bloccato
Preparazione al lancio di una flotta di droni 🚀
Preparazione al lancio di una flotta di droni 🚀
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION