Thread in JAVA: Ciclo di Vita, Sincronizzazione e Concorrenza - Tecnologia A4 M1 L02 | Prof. Giagnotti
Tecnologia • Anno 4 • Modulo 1 • Lezione 02
✍ Autore: Prof. Giagnotti

Thread in JAVA: Ciclo di Vita, Sincronizzazione e Concorrenza

Da un main single-thread a un'app che scarica, calcola e aggiorna l'UI contemporaneamente: come funziona davvero un Thread in Java, come nasce, dorme, aspetta e muore dentro la JVM.

⏱ 60 min 🧠 Thread • Runnable • synchronized • Executor
OBIETTIVI DI APPRENDIMENTO
✓Distinguere processo e thread dentro la JVM e capire perché Java è nativamente multithreaded
✓Conoscere gli stati di un Thread Java (NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED) e le transizioni
✓Creare thread correttamente con estensione Thread vs implementazione Runnable / lambda e avviarli con start()
✓Gestire race condition, sincronizzazione, deadlock e scrivere codice concorrente sicuro con Executor e Virtual Thread

Quando lanciate java Main pensate di far partire un programma. In realtà state accendendo una piccola città: la JVM parte, crea il main thread, il garbage collector, il Finalizer, il JIT compiler. Io vi dico sempre una cosa in lab: se il vostro programma fa tutto nel main, state usando lo 0.1% della JVM. Oggi impariamo a usarla tutta.

1. Programmi, processi e thread: perché Java non vive di solo main

📌 Definizione da ricordare

Processo = istanza di JVM con suo heap. Thread = flusso leggero dentro la JVM che condivide heap ma ha suo stack e program counter. In Java ogni thread è un oggetto java.lang.Thread.

Il programma App.class è statico. Il processo è la JVM che lo carica: ha heap, Metaspace, file aperti. Il thread è l'unità che davvero gira sul core. Io ve lo spiego così da 10 anni e funziona sempre.

💬 Tip del docente – tecnologia concreta

Io ho pensato di fare così per farvelo ricordare all'interrogazione: aprite IntelliJ, lanciate un programma vuoto e guardate in Debug > Threads: vedrete almeno 5 thread anche se non ne avete creato uno. Io lo farei così per spiegarlo alla Maturità: "Il programma è la fabbrica, il processo è la fabbrica accesa con corrente e operai (JVM), i thread sono gli operai che lavorano in parallelo sullo stesso magazzino (heap)". Se dite che thread e processo sono uguali, vi fermo subito.

🎯 Esempio in classe – UI che si blocca

Avete una app Swing/JavaFX che scarica un file nel main thread? L'interfaccia si freeza. Perché? Un solo thread fa tutto: UI + rete. Soluzione: un thread worker scarica, il main resta RUNNABLE per ridisegnare. Questo è il motivo #1 per cui in Java si usano i thread.

2. Multithreading, multiprocessing e la JVM

Una JVM single-core fa time-sharing tra thread Java, esattamente come il SO tra processi. Con più core, fa vero parallelismo. La JVM mappa i Java Thread su thread nativi del SO (1:1 su HotSpot).

TermineSignificato in JavaObiettivo / Quando
MultithreadingPiù Thread nella stessa JVM, condividono heap, stack separatiReattività + sfruttare multi-core senza duplicare memoria
Multiprocessing (multi-JVM)Più processi JVM separati, heap isolatiIsolamento totale, ma costo RAM alto
Concorrenza vs ParallelismoConcorrenza = gestire più task (interleaving), Parallelismo = eseguirli davvero insieme su core diversiJava fa entrambi: Executor decide
💬 Tip del docente

Io ho pensato di fare così quando correggo: se scrivete "i thread non condividono nulla" vi tolgo mezzo punto. Io lo farei così per ricordarlo: "In Java i thread condividono l'heap (oggetti), non lo stack (variabili locali)". Guardate VisualVM > Threads: vedete RUNNABLE che saltano da un core all'altro.

⚠️ Errore comune – heap condiviso = guai

Due thread che fanno conto++ sullo stesso oggetto condividono la stessa variabile in heap. Senza sincronizzazione perdete incrementi. Non è un bug raro, è la normalità se non usate synchronized o AtomicInteger.

3. Il ciclo di vita: gli stati di un Thread in Java

Un Thread Java non ha 3 stati, ne ha 6. Li trovate in Thread.State. Li dovete sapere a memoria per l'esame, con le frecce.

NEW RUNNABLE BLOCKED TIMED_WAITING sleep / join / wait(t) WAITING TERMINATED start() sleep(ms) synchronized wait()/join() run() finita exit timeout / notify()
Stato JavaCosa significa davvero
NEWOggetto Thread creato con new Thread(), ma start() non ancora chiamato. Non in scheduler.
RUNNABLEDopo start(). In realtà è Ready + Running messi insieme. La JVM decide quando dargli CPU.
BLOCKEDBloccato su monitor: vuole entrare in un blocco synchronized già occupato.
WAITINGHa chiamato wait(), join() senza timeout, LockSupport.park(). Aspetta notify() di un altro thread, infinito.
TIMED_WAITINGCome WAITING ma con timer: Thread.sleep(ms), wait(ms), join(ms), LockSupport.parkNanos().
TERMINATEDMetodo run() finito o eccezione non catturata. Non torna più RUNNABLE.
🎯 Esempio – perché TIMED_WAITING salva la CPU

Fate sleep(1000) invece di while(System.currentTimeMillis()...). Nel primo caso il thread va in TIMED_WAITING e non consuma CPU. Nel secondo resta RUNNABLE e brucia un core al 100%. Io lo vedo ogni anno in verifica: busy-wait = bocciatura diretta dell'esercizio.

4. Cosa salva la JVM per un thread: stack, PC e TCB

📌 Definizione da esame – TCB del thread Java

Il Thread Control Block in JVM è leggero: Program Counter Java, stack dei frame (metodi chiamati), riferimento a heap, stato, priorità, thread-local. Context switch tra thread Java = salva solo stack + registri, non heap: ecco perché è 10-100x più veloce di un cambio processo.

Campo del threadContenuto praticoPerché serve nel vostro lab Java
TID / NomeID + name (es. "Thread-3 - Downloader")Per debug, log, Thread.currentThread().getName()
StatoNEW/RUNNABLE/BLOCKED...Thread.getState() in VisualVM
Program Counter JVMBytecode successivo da eseguireAl ritorno da Waiting riparte esatto
Java StackFrame di chiamate (metodi, variabili locali)StackOverflowError se ricorsione infinita per thread
Priorità (1-10)MIN_PRIORITY 1, NORM 5, MAX 10Hint per scheduler, non garanzia
Heap condivisoOggetti, staticQui nascono le race condition
💬 Metodo Giagnotti – come spiegarlo

Io ho pensato di fare così a lezione di Tecnologia: apro jconsole e vi faccio vedere i thread. Clicco su "Downloader-1" e vi mostro stack trace, stato BLOCKED su synchronized. Io lo farei così all'interrogazione: "Prof, il TCB del thread Java è come il segnalibro di un libro: salva pagina (PC) e appunti sul tavolo (stack), ma la libreria (heap) resta condivisa". Se dite questa frase, vi do il mezzo punto in più.

⚠️ Attenzione – costo thread

Ogni thread Java mangia ~1MB di stack di default. 1000 thread = 1GB solo di stack. Ecco perché oggi non creiamo più thread a mano, usiamo pool. Altrimenti OutOfMemoryError: unable to create native thread.

5. Come creare un thread in Java (i 2 metodi + quello che uso io)

Ci sono 2 modi ufficiali, ma uno è quello che vi chiedo io in verifica. Non esiste start() senza run().

Metodo 1 – Eredità

extends Thread

Semplice ma spreca ereditarietà. Non potete estendere altro. Lo vedo nei vecchi libri.

Metodo 2 – Quello giusto

implements Runnable / lambda

Separa task da thread. Riutilizzabile con Executor. È quello che uso io al 90% in lab.

JAVA · 2 modi ufficiali + errore da non fare
 METODO 1: extends Thread - sconsigliato ma da sapere
class MioThread extends Thread {
    @Override
    public void run() {
        System.out.println("Sono " + Thread.currentThread().getName() + " - ID: " + Thread.currentThread().getId());
    }
}

 METODO 2: implements Runnable - quello che voglio io
class MioTask implements Runnable {
    private String nome;
    public MioTask(String nome){ this.nome = nome; }
    @Override
    public void run() {
        for(int i=1;i<=3;i++){
            System.out.println(nome + " -> " + i + " su " + Thread.currentThread().getName());
            try { Thread.sleep(500); } catch (InterruptedException e){ Thread.currentThread().interrupt(); }
        }
    }
}

public class MainThread {
    public static void main(String[] args) {
        // ERRORE CHE VEDO SEMPRE: chiamare run() invece di start()
        // MioThread t = new MioThread(); t.run(); // SBAGLIATO: resta nel main, non è un thread!

        // GIUSTO:
        Thread t1 = new MioThread();
        t1.setName("Downloader-1");
        t1.start(); // NEW -> RUNNABLE

        Thread t2 = new Thread(new MioTask("Calcolo-A"));
        t2.start();

        // METODO CHE USO IO: lambda + Runnable - pulito e moderno
        Thread t3 = new Thread(() -> {
            System.out.println("Task lambda su " + Thread.currentThread().getName());
        }, "Lambda-Worker");
        t3.start();

        System.out.println("Main continua, non aspetta. Stato t1: " + t1.getState());
    }
}
💬 Tip lab Tecnologia

Io ho pensato di fare così in lab: vi faccio scrivere prima run() e poi start() e vi faccio stampare Thread.currentThread().getName(). Se vedete due volte "main", avete chiamato run() e non start(). Io lo farei così per farlo sembrare vostro: dite "Prof, start() crea il thread nativo e mette NEW->RUNNABLE, run() è solo un metodo normale". Nelle app moderne non faccio mai new Thread() in loop, uso Executor, ve lo mostro dopo.

JAVA · Caso d'uso reale: download + UI responsive
 Caso d'uso: 3 download paralleli senza bloccare main
import java.util.concurrent.atomic.AtomicInteger;

public class DownloadManager {
    static AtomicInteger completati = new AtomicInteger(0);

    static Runnable creaDownload(String url){
        return () -> {
            String threadName = Thread.currentThread().getName();
            System.out.println("["+threadName+"] Inizio download: " + url);
            try {
                // Simulo rete lenta -> TIMED_WAITING
                Thread.sleep((long)(Math.random()*2000 + 500));
            } catch (InterruptedException e){ return; }
            int tot = completati.incrementAndGet();
            System.out.println("["+threadName+"] Finito " + url + " - Totale: " + tot + "/3");
        };
    }

    public static void main(String[] args) throws InterruptedException {
        Thread d1 = new Thread(creaDownload("foto1.jpg"), "DL-1");
        Thread d2 = new Thread(creaDownload("video.mp4"), "DL-2");
        Thread d3 = new Thread(creaDownload("doc.pdf"), "DL-3");

        long start = System.currentTimeMillis();
        d1.start(); d2.start(); d3.start();

        // Main fa altro nel frattempo
        System.out.println("[MAIN] Interfaccia libera, posso muovere il mouse...");

        // Aspetto che finiscano -> WAITING
        d1.join(); d2.join(); d3.join();

        System.out.println("[MAIN] Tutti finiti in " + (System.currentTimeMillis()-start) + "ms vs 4500ms sequenziale");
    }
}

6. Sincronizzazione: race condition, mutex e deadlock

Questo è il cuore dell'esame. Se due thread condividono un oggetto e almeno uno scrive, avete una corsa. Senza controllo, il risultato è casuale.

ProblemaCome si vedeSoluzione Java che voglio in verifica
Race Conditioncounter++ fa 1000+1000 = 1847, non 2000synchronized, AtomicInteger, Lock
VisibilityThread non vede modifica di un altro per cache CPUvolatile, synchronized (happens-before)
DeadlockThread A ha lock X aspetta Y, B ha Y aspetta X. Entrambi WAITING per sempreOrdine dei lock, tryLock con timeout
StarvationThread bassa priorità non entra mai in synchronizedReentrantLock fair=true
JAVA · Race condition + fix synchronized
class ContoCorrente {
    private int saldo = 0;

    // VERSIONE SBAGLIATA - 10 studenti su 10 lo sbagliano al primo colpo
    // public void deposita(int v){ saldo += v; } // read-modify-write NON atomico!

    // VERSIONE GIUSTA 1: synchronized - monitor dell'oggetto
    public synchronized void depositaSync(int v){
        saldo += v; // ora atomico rispetto ad altri synchronized sullo stesso oggetto
    }

    // VERSIONE GIUSTA 2: blocco esplicito - quello che uso io quando devo spiegare
    private final Object lock = new Object();
    public void depositaLock(int v){
        synchronized(lock){
            saldo += v;
        }
    }

    // VERSIONE GIUSTA 3: moderna, senza lock
    // private AtomicInteger saldoAtom = new AtomicInteger(0);
    // saldoAtom.addAndGet(v);

    public int getSaldo(){ return saldo; }
}

public class TestRace {
    public static void main(String[] args) throws InterruptedException {
        ContoCorrente conto = new ContoCorrente();

        Runnable task = () -> {
            for(int i=0;i<10000;i++) conto.depositaSync(1);
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start(); t2.start();
        t1.join(); t2.join();

        // Se usi metodo non synchronized vedrai 18743, 19231... casuale!
        System.out.println("Saldo atteso 20000, reale: " + conto.getSaldo());
    }
}
JAVA · Deadlock classico - da saper disegnare
public class DeadlockDemo {
    static final Object forchetta1 = new Object();
    static final Object forchetta2 = new Object();

    public static void main(String[] args) {
        Thread filosofo1 = new Thread(() -> {
            synchronized(forchetta1){
                System.out.println("Filosofo1 ha forchetta1, vuole forchetta2...");
                try{ Thread.sleep(100); } catch(Exception e){}
                synchronized(forchetta2){ // aspetta per sempre se l'altro ha forchetta2
                    System.out.println("Filosofo1 mangia");
                }
            }
        }, "Filosofo-1");

        Thread filosofo2 = new Thread(() -> {
            synchronized(forchetta2){ // ORDINE INVERSO -> DEADLOCK!
                System.out.println("Filosofo2 ha forchetta2, vuole forchetta1...");
                try{ Thread.sleep(100); } catch(Exception e){}
                synchronized(forchetta1){
                    System.out.println("Filosofo2 mangia");
                }
            }
        }, "Filosofo-2");

        filosofo1.start(); filosofo2.start();

        // Come lo trovo? jps + jstack PID -> vi dice "Found 1 deadlock"
        // Fix: sempre stesso ordine di lock!
    }
}
💬 Tip del docente – come lo spiego io all'esame

Io ho pensato di fare così quando mi chiedete del deadlock: vi disegno alla lavagna due cerchi che si abbracciano. Io lo farei così per farlo sembrare vostro: "Prof, il deadlock ha 4 condizioni di Coffman: mutua esclusione, hold-and-wait, no preemption, attesa circolare. Se rompo una, niente deadlock. In Java rompo l'attesa circolare prendendo i lock sempre nello stesso ordine". Se dite Coffman, vi metto subito 8.

⚠️ Errore comune – wait() senza loop

Mai fare if(!condizione) wait(). Sempre while(!condizione) wait(). Perché? Spurious wakeup e notify() di un altro thread. Se usate if, vi risvegliate con condizione falsa e andate in eccezione. È l'errore che trovo nel 70% dei vostri compiti.

7. Multithreading moderno: Executor, Pool e Virtual Thread

Dal 2014 in poi nessuno crea più new Thread() a mano in produzione. Si usano pool. Dal 2023 con Java 21, i Virtual Thread cambiano tutto: costano pochi KB, non 1MB.

Strumento JavaCosa faQuando lo uso io
ExecutorServiceGestisce pool di thread, riuso, codaSempre, al posto di new Thread(). Es. newFixedThreadPool(4)
Callable + FutureCome Runnable ma ritorna valore e lancia eccezioniQuando il thread deve restituire risultato
Virtual Thread (Java 21+)Thread leggerissimo gestito da JVM, 1M thread = okServer, 10k connessioni, I/O bound
CompletableFuturePipeline asincrona, thenApply, thenCombineMicroservizi, chiamate REST parallele
🎯 Priority boost moderno – come Java gestisce 10k client

Server vecchio: 1 thread per client = 10k thread = 10GB RAM = crash. Server con Executor fixed 100 = coda, lento. Server con Virtual Thread: Thread.ofVirtual().start(() -> gestisciClient) → ogni client ha suo thread da pochi KB, quando fa I/O va in WAITING e la JVM lo parcheggia. Così il server sembra avere 10k thread ma ne usa 8 reali. È il motivo per cui oggi Java ha battuto Node su concorrenza.

JAVA · Executor + Virtual Thread - codice da produzione
import java.util.concurrent.*;
import java.util.*;

public class ModernoConcorrente {

    // METODO CLASSICO CHE USO IN 4A: ExecutorService
    public static void demoExecutor() throws Exception {
        // Io ho pensato di fare così: 4 thread = numero core della vostra VM in lab
        ExecutorService pool = Executors.newFixedThreadPool(4);

        List<Future<Integer>> futures = new ArrayList<>();
        for(int i=0;i<8;i++){
            int taskId = i;
            Future<Integer> f = pool.submit(() -> {
                System.out.println("Task " + taskId + " su " + Thread.currentThread().getName());
                Thread.sleep(500);
                return taskId * taskId;
            });
            futures.add(f);
        }

        for(Future<Integer> f : futures){
            System.out.println("Risultato: " + f.get()); // WAITING finché pronto
        }

        pool.shutdown(); // IMPORTANTE: altrimenti JVM non termina!
    }

    // METODO 2024+ CHE VI CHIEDERANNO AL COLLOQUIO: Virtual Thread
    public static void demoVirtualThread() throws Exception {
        // Con Java 21+
        try (ExecutorService vPool = Executors.newVirtualThreadPerTaskExecutor()) {
            for(int i=0;i<1000;i++){
                int id = i;
                vPool.submit(() -> {
                    try{
                        Thread.sleep(100); // I/O simulato: qui il virtual thread si stacca dal carrier
                        if(id % 200 == 0) System.out.println("Virtual task " + id + " su " + Thread.currentThread());
                    } catch(InterruptedException e){ Thread.currentThread().interrupt(); }
                });
            }
        } // chiude automatico
        System.out.println("1000 virtual thread finiti senza 1GB di RAM");
    }

    public static void main(String[] args) throws Exception {
        System.out.println("--- Executor Classico ---");
        demoExecutor();
        System.out.println("\n--- Virtual Thread (Java 21+) ---");
        // demoVirtualThread(); // scommentate se avete JDK 21
    }
}
💬 Tip lab Tecnologia

Io ho pensato di fare così in lab: vi faccio lanciare 1000 new Thread() e vi faccio vedere OutOfMemoryError. Poi vi faccio lanciare 1000 virtual thread e vedete che gira. Io lo farei così per farlo sembrare vostro: dite "Prof, con i platform thread riuso i thread per non pagare il costo di creazione, con i virtual thread non serve riuso perché costano poco". Se dite questa frase in sede d'esame, vi metto il bonus.

8. Verifica e laboratorio mentale

🔬 Attività – carta e penna (da saper fare senza PC)

Esercizi che escono in verifica scritta, 15 minuti:

  1. Disegna il diagramma NEW → RUNNABLE → (BLOCKED / WAITING / TIMED_WAITING) → TERMINATED con le 6 frecce etichettate (start, synchronized, wait, sleep, notify, fine run).
  2. Dati 2 thread che fanno saldo++ 10000 volte su stesso ContoCorrente senza synchronized: quale valore finale ti aspetti? Perché può essere <20000? Mostra il read-modify-write.
  3. Codice: Thread t = new Thread(() -> {...}); t.run(); t.run(); Cosa succede? In che stato è t dopo? Perché non è multithreading?
  4. Spiega perché in Java wait() deve stare dentro synchronized e dentro while(condizione). Cosa succede con if?
  5. Caso d'uso: server chat con 5000 client. Perché con Thread classici crasha e con Virtual Thread (Java 21) regge? Calcola memoria: 5000 x 1MB vs 5000 x 5KB.
✅ Soluzione ragionata – chiave

1) Guardate svg sopra. 2) Race: T1 legge 100, T2 legge 100, entrambi scrivono 101 invece di 102. Incremento perso, non atomico. Valore atteso <20000, non deterministico. 3) run() è chiamata diretta nel main, non crea thread nativo. Stato resta NEW (start mai chiamato). Non c'è parallelismo, solo due chiamate sequenziali. 4) wait() rilascia monitor, quindi deve possederlo (synchronized). While serve per spurious wakeup + condizione cambiata da altri. Con if rischi di procedere con condizione falsa. 5) Platform: 5000MB stack = OOM. Virtual: 5000*5KB=25MB. Inoltre quando client fa I/O, virtual thread si smonta dal carrier → 8 carrier servono 5000 client.

✓Processo ≠ Thread in Java: Processo = JVM con heap, Thread = flusso con suo stack che condivide heap.
✓6 stati Java: NEW, RUNNABLE, BLOCKED (su synchronized), WAITING (wait/join), TIMED_WAITING (sleep), TERMINATED + transizioni start/wait/notify/sleep.
✓Creazione: extends Thread (vecchio) vs implements Runnable / lambda (quello che uso io). start() crea thread, run() no.
✓Sincronizzazione: Race su heap condiviso → synchronized / Lock / Atomic. wait() sempre in while + synchronized. Deadlock = ordine lock diverso.
✓Priorità Java: 1-10, hint non garanzia. Lo scheduler vero è del SO, non della JVM.
✓Moderno: ExecutorService per riuso thread, Future/Callable per risultati, Virtual Thread Java 21 per 10k connessioni senza OOM.
✓Casi d'uso reali: UI responsive (worker thread), download paralleli, server chat, pipeline dati, calcolo parallelo su multi-core.