CodeGym /Corsi /JAVA 25 SELF /Stati e ciclo di vita di un thread

Stati e ciclo di vita di un thread

JAVA 25 SELF
Livello 51 , Lezione 2
Disponibile

1. Stati principali di un thread in Java

In Java un thread non è semplicemente «avviato» o «fermato». Ha un intero ciclo di vita e, in ogni fase, si comporta in modo diverso. Comprendere queste fasi — è la chiave per scrivere applicazioni multithread stabili e per effettuare il debug di strani «blocchi» e «terminazioni inattese».

Quali stati esistono?

Java definisce i seguenti stati principali del thread (sono elencati nell’enumerazione Thread.State):

Stato Descrizione
NEW
Il thread è stato creato ma non ancora avviato (new Thread(...), ma start() non è stato chiamato)
RUNNABLE
Il thread è pronto per l’esecuzione oppure è in esecuzione in questo momento
BLOCKED
Il thread sta aspettando il rilascio del monitor (bloccato su un blocco synchronized)
WAITING
Il thread attende che un altro thread lo «risvegli» (ad esempio tramite Object.wait())
TIMED_WAITING
Il thread attende con timeout (ad esempio, Thread.sleep(1000), wait(1000), join(1000))
TERMINATED
Il thread ha terminato l’esecuzione

Curiosità:
Nei libri e negli articoli più vecchi potresti trovare altre denominazioni o schemi leggermente diversi. Ma da Java 5+ questi stati sono considerati lo standard.

Schema visivo del ciclo di vita del thread

stateDiagram-v2
    [*] --> NEW
    NEW --> RUNNABLE: start()
    RUNNABLE --> BLOCKED: tentativo di entrare in synchronized, ma il monitor è occupato
    BLOCKED --> RUNNABLE: il monitor è stato rilasciato
    RUNNABLE --> WAITING: wait(), join()
    RUNNABLE --> TIMED_WAITING: sleep(), wait(timeout), join(timeout)
    WAITING --> RUNNABLE: notify()/notifyAll(), join() è terminato
    TIMED_WAITING --> RUNNABLE: il timeout è scaduto / notify()/notifyAll()
    RUNNABLE --> TERMINATED: run() è terminato
    WAITING --> TERMINATED: run() è terminato (raramente)
    TIMED_WAITING --> TERMINATED: run() è terminato (raramente)

2. Metodi di gestione del thread

Sospensione: Thread.sleep(long millis)

A volte un thread ha bisogno di «dormire», per non intralciare gli altri o per attendere un evento. Il metodo Thread.sleep(ms) porta il thread nello stato

TIMED_WAITING
per il numero di millisecondi specificato.

System.out.println("Il thread si addormenta per 2 secondi...");
Thread.sleep(2000); // Dormiamo per 2 secondi
System.out.println("Il thread si è svegliato!");
  • Al termine del sonno il thread torna nello stato
    RUNNABLE
    (pronto a lavorare).
  • Se il thread viene interrotto durante il sonno, viene lanciata InterruptedException.

Attendere la fine di un altro thread: join()

Talvolta non basta avviare un thread: occorre aspettare che finisca il lavoro. Per questo in Java esiste il metodo join():

Thread t = new Thread(() -> {
    System.out.println("Sto lavorando...");
    try { Thread.sleep(1000); } catch (InterruptedException e) {}
    System.out.println("Fatto!");
});
t.start();
System.out.println("Attendo il completamento del thread t...");
t.join(); // Il thread corrente (ad esempio main) attende t
System.out.println("Il thread t è terminato!");

Qui il thread principale inizia ad attendere non appena viene chiamato join(). Per tutto questo tempo si trova nello stato

WAITING
, finché il thread t non termina l’esecuzione. Esiste anche la variante con timeout — join(long millis). In tal caso il thread attende per un tempo limitato e il suo stato sarà
TIMED_WAITING
.

Interruzione del thread: interrupt()

Capita di dover chiedere educatamente a un thread di terminare il lavoro, ad esempio se l’utente ha premuto «Annulla». A questo serve il metodo interrupt():

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Lavoriamo...
    }
    System.out.println("Il thread è stato terminato tramite interruzione!");
});
t.start();
// ... dopo un po’:
t.interrupt(); // Segnale al thread: "è ora di fermarsi"

È importante capire che la chiamata a interrupt() non uccide il thread all’istante. Si limita a impostare un flag speciale. Il thread stesso deve controllare periodicamente questo flag tramite isInterrupted() e terminare da solo. Se in quel momento il thread si trova in stato di sonno (sleep()) o di attesa (wait()), non solo vedrà il flag, ma riceverà immediatamente l’eccezione InterruptedException. È così che in Java è organizzato il modo «corretto» di fermare i thread: il programma non li interrompe con la forza, ma li avvisa che è ora di terminare.

3. Esempi di transizioni tra stati

Vediamo con alcuni esempi come un thread «viaggia» tra i suoi stati.

Esempio 1:
NEW
RUNNABLE
TERMINATED

Thread t = new Thread(() -> System.out.println("Ciao!"));
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (oppure TERMINATED, se il thread è molto veloce)
t.join();
System.out.println(t.getState()); // TERMINATED

Esempio 2:
RUNNABLE
TIMED_WAITING
RUNNABLE
TERMINATED

Thread t = new Thread(() -> {
    try {
        System.out.println("Mi addormento...");
        Thread.sleep(1000); // TIMED_WAITING
        System.out.println("Mi sono svegliato!");
    } catch (InterruptedException e) {
        System.out.println("Thread interrotto!");
    }
});
t.start();

Esempio 3:
RUNNABLE
WAITING
con join()

Thread t1 = new Thread(() -> {
    try { Thread.sleep(500); } catch (InterruptedException ignored) {}
    System.out.println("t1 completato");
});
Thread t2 = new Thread(() -> {
    try {
        t1.join(); // t2 aspetta t1, si trova in WAITING
        System.out.println("t2 ha aspettato t1");
    } catch (InterruptedException ignored) {}
});
t1.start();
t2.start();

Esempio 4:
BLOCKED

Object lock = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lock) {
        try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
        System.out.println("t1 è uscito dal blocco synchronized");
    }
});
Thread t2 = new Thread(() -> {
    synchronized (lock) {
        System.out.println("t2 è entrato in synchronized");
    }
});
t1.start();
Thread.sleep(100); // Lasciamo che t1 acquisisca il lock
t2.start();
Thread.sleep(100); // Lasciamo che t2 provi a entrare in synchronized
System.out.println("Stato di t2: " + t2.getState()); // BLOCKED

4. Come conoscere lo stato di un thread? Metodi isAlive() e getState()

  • isAlive() — restituisce true se il thread è avviato e non è ancora terminato (
    TERMINATED
    restituisce già false).
  • getState() — restituisce lo stato corrente del thread (un valore dell’enumerazione Thread.State).
Thread t = new Thread(() -> {});
System.out.println(t.isAlive()); // false (NEW)
t.start();
System.out.println(t.isAlive()); // true (RUNNABLE/WAITING/...)
t.join();
System.out.println(t.isAlive()); // false (TERMINATED)

5. Perché non si può «uccidere» direttamente un thread e altri consigli pratici

Nessun metodo «kill»!

In Java non esiste un metodo che consenta di «uccidere» un thread a comando. Perché? Perché non è sicuro: se il thread detiene qualche risorsa (file, connessione, lock), la sua terminazione forzata può lasciare il sistema in uno stato non consistente.

Metodi obsoleti: stop(), suspend(), resume()

Nelle versioni antiche di Java esistevano i metodi stop(), suspend(), resume(). Oggi sono contrassegnati come @Deprecated, e il loro utilizzo è vivamente sconsigliato. Perché?

  • stop() può uccidere il thread in qualsiasi momento, lasciando i dati in uno stato non consistente.
  • suspend() può «congelare» un thread che detiene un lock, e allora l’intero programma si blocca.
  • resume() talvolta non riesce a «scongelare» un thread se questo è già terminato.

Approccio moderno:
Il thread deve terminare correttamente da solo, reagendo al flag di interruzione (isInterrupted()) o ad altri segnali.

Buone pratiche

  • Non chiamare metodi contrassegnati come obsoleti.
  • Usa il flag di interruzione per fermare il thread (interrupt() e controllo di isInterrupted()).
  • Monitora gli stati dei thread durante il debug — aiuta a individuare blocchi e deadlock.
  • Non dimenticare join(), se devi attendere il completamento del lavoro di un thread.

6. Errori tipici nel lavoro con il ciclo di vita di un thread

Errore n. 1: avviare nuovamente un thread.
In Java un thread può essere avviato una sola volta. Se chiami start() una seconda volta — otterrai IllegalThreadStateException. Se devi ripetere un’attività — crea un nuovo oggetto Thread.

Thread t = new Thread(() -> {});
t.start();
t.start(); // Genererà un’eccezione!

Errore n. 2: confondere run() e start().
Chiamare direttamente run() non esegue il codice in un nuovo thread — viene eseguito nel thread corrente (ad esempio in main). Solo start() avvia davvero un nuovo thread.

Errore n. 3: non gestire InterruptedException.
Se un thread dorme o attende e viene interrotto, verrà lanciata InterruptedException. Se la ignori, il thread può «addormentarsi per sempre» o terminare in modo inatteso.

Errore n. 4: non verificare lo stato del thread.
A volte il programma si blocca perché un thread aspetta un altro che è già terminato o che non inizierà mai a lavorare. Usa getState() e isAlive() per la diagnosi.

Errore n. 5: utilizzare metodi obsoleti di gestione dei thread.
I metodi stop(), suspend(), resume() — sono dannosi. Non usarli, anche se la tentazione è quella di «sistemare tutto in fretta».

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION