W3docs

Ciclo di vita dei thread Java

Gli stati di un thread Java — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — e come cambiano.

Un thread Java non ha molti stati — sei, tutti valori dell'enum Thread.State. Ma questi sei sono il vocabolario dei thread dump, dei profiler e di ogni indagine del tipo "perché il mio programma è bloccato" che farai mai. Sapere cosa significa ogni stato e quali transizioni sono possibili è ciò che trasforma un thread dump da un muro di stack trace in una diagnosi.

I sei stati

// java.lang.Thread.State — the six possible thread states
public enum State {
  NEW,                  // created, never started
  RUNNABLE,             // started; running or ready to run on a CPU
  BLOCKED,              // waiting for a monitor lock to enter a synchronized block
  WAITING,              // parked indefinitely (Object.wait, Thread.join, LockSupport.park)
  TIMED_WAITING,        // parked with a timeout (sleep, wait(ms), join(ms), park(nanos))
  TERMINATED            // run() has returned
}

Le transizioni consentite tra di essi formano un semplice ciclo di vita:

   NEW
    |  start()
    v
RUNNABLE  <----------+--------+-------+
    |   |            |        |       |
    |   | enters     | wakes  | timeout
    |   v sync       | from   | expires
    |  BLOCKED       | wait/  |
    |   |            | join   |
    |   | acquires   |        |
    |   v lock       |        |
    +-> RUNNABLE     |        |
    |                |        |
    | wait/join/park |        |
    v                |        |
WAITING -------------+        |
    |                         |
    | wait(ms)/join(ms)/sleep |
    v                         |
TIMED_WAITING ----------------+
    |
    | run() returns
    v
TERMINATED

Ogni stato corrisponde a qualcosa di visibile in un thread dump. Esaminiamoli uno per uno.

NEW

Un Thread che hai costruito ma su cui non hai mai chiamato start(). Nessuna risorsa del sistema operativo è stata allocata; nulla è in esecuzione. Le uniche transizioni possibili sono:

  • start()RUNNABLE
  • Il thread viene raccolto dal garbage collector senza mai essere eseguito

Puoi chiamare start() esattamente una volta. Una seconda chiamata lancia IllegalThreadStateException.

RUNNABLE

"Il thread è attivo ed è in esecuzione su una CPU in questo momento oppure è pronto per essere eseguito." Java unifica gli stati "running" e "runnable" del sistema operativo in un unico stato — non è possibile capire solo da Thread.State se il thread sta consumando CPU in quel momento. Il pianificatore del sistema operativo decide quali thread RUNNABLE ottengono effettivamente un core in ogni istante.

Un thread RUNNABLE è anche lo stato in cui si trova un thread bloccato su I/O (InputStream.read, Socket.read, FileChannel.read). Questo sorprende molti: il thread è "pronto per essere eseguito" solo nel senso che nulla nella JVM lo sta bloccando. Il sistema operativo sa che il thread sta aspettando il disco; la JVM no, quindi riporta RUNNABLE. Se vedi un thread dump in cui un thread è RUNNABLE e il suo frame superiore è socketRead0 o simile, il thread è bloccato su una syscall — non sta consumando CPU.

Attenzione

RUNNABLE non significa "occupato." È lo stato più frainteso in un thread dump: un thread parcheggiato all'interno di una chiamata I/O bloccante (socketRead0, FileInputStream.read) riporta RUNNABLE anche se usa zero CPU. Non concludere che un thread sia sotto carico dal suo stato — leggi il suo frame superiore nello stack, o campiona con un profiler.

BLOCKED

Il thread è fermo alla porta di un blocco synchronized in attesa del monitor lock. Un altro thread ce l'ha; questo si è messo in coda. Non appena il detentore lo rilascia, uno dei thread in attesa acquisisce il lock e torna a RUNNABLE.

BLOCKED è specifico di synchronized — il meccanismo di lock intrinseco integrato nella JVM. Il codice in attesa di un ReentrantLock non mostra BLOCKED; mostra WAITING (perché ReentrantLock è implementato sopra LockSupport.park). È una distinzione piccola ma importante quando si leggono i dump.

La firma classica in un thread dump per BLOCKED:

"worker-3" #19 prio=5 ... waiting for monitor entry
   java.lang.Thread.State: BLOCKED (on object monitor)
   at com.acme.Cache.put(Cache.java:42)
   - waiting to lock <0x000000076ab8e220> (a java.util.HashMap)
   at com.acme.Cache.miss(Cache.java:67)

Due informazioni: quale monitor stai aspettando (<0x000000076ab8e220>) e quale metodo è fermo alla porta. Cerca nello stesso dump - locked <0x000000076ab8e220> e troverai il thread che lo detiene.

WAITING

Il thread ha scelto di aspettare indefinitamente. Tre cose mettono un thread qui:

  • Object.wait() — rilascia il monitor e si parcheggia finché qualcuno chiama notify/notifyAll.
  • Thread.join() — senza timeout, si parcheggia finché il thread target termina.
  • LockSupport.park() — la primitiva su cui sono costruiti ReentrantLock.lock(), await(), BlockingQueue.take() e tutto java.util.concurrent.

Un thread WAITING consuma praticamente nessuna risorsa oltre al suo stack. Non effettuerà transizioni finché qualcun altro non fa qualcosa — una notify, il completamento del thread target, un LockSupport.unpark. Se nulla lo sblocca mai, rimane lì per sempre. Così appaiono i deadlock silenziosi in un dump: due thread, entrambi WAITING, entrambi in possesso di ciò che l'altro vuole.

TIMED_WAITING

Stessa idea di WAITING, ma con una scadenza. Il thread si sveglierà da solo allo scadere del timeout anche se non succede nient'altro. Le cose che producono TIMED_WAITING:

  • Thread.sleep(ms)
  • Object.wait(ms)
  • Thread.join(ms)
  • LockSupport.parkNanos(...), LockSupport.parkUntil(...)
  • BlockingQueue.poll(timeout, unit), Future.get(timeout, unit), ecc.

Se un thread è stabilmente in TIMED_WAITING per la durata che hai specificato, non è un bug. Se rimane lì oltre il timeout, è stato riparcheggiato — qualcuno ha chiamato wait(1000) in un ciclo oppure la coda è ancora vuota.

TERMINATED

run() è tornato (normalmente o per eccezione). Il thread è terminato; non può essere riavviato. t.isAlive() restituisce false. Puoi ancora leggere il suo nome e ID per scopi di log/debug, ma il thread stesso è finito.

Leggere lo stato dal proprio codice

Thread.State è pubblicamente interrogabile, ma il valore è uno snapshot — può cambiare tra la chiamata e il suo utilizzo. In produzione non ci si dirama quasi mai su di esso; lo si usa per logging e diagnostica. La JVM espone anche ThreadMXBean per thread dump completi, che è ciò che la maggior parte dei dashboard JMX visualizza.

Thread t = new Thread(() -> doWork(), "worker");
System.out.println(t.getState());          // NEW
t.start();
System.out.println(t.getState());          // RUNNABLE (or TIMED_WAITING/BLOCKED/etc., racy)
t.join();
System.out.println(t.getState());          // TERMINATED

Un esempio pratico: osservare ogni stato

Il programma seguente crea thread che si bloccano ciascuno in uno stato diverso, poi stampa in quale stato si trovano.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Ognuno dei sei stati era raggiungibile dal codice nello stesso programma. NEW e TERMINATED sono i casi limite; i quattro centrali (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING) sono quelli che vedrai in un thread dump reale.
  • Il blocked-thread ha riportato BLOCKED perché il holder possedeva il monitor synchronized. Se avessimo usato un ReentrantLock al posto, lo stesso percorso di codice avrebbe riportato WAITING (poiché Lock.lock() si parcheggia tramite LockSupport). Il nome dello stato ti dice che tipo di attesa, non solo "questo thread è bloccato."
  • Il waiting-thread sarebbe rimasto in WAITING per sempre se main non avesse chiamato cond.notify(). Lo stato WAITING non ha timeout — qualcun altro deve svegliarlo. È esattamente così che una notify mancata produce un deadlock che nessuna eccezione riporta mai.
  • Il thread che consumava CPU ha riportato RUNNABLE sia che fosse effettivamente in esecuzione su un core sia che stesse semplicemente aspettando nella coda di esecuzione. La JVM non distingue "in esecuzione" da "pronto"; il sistema operativo sì. Se hai bisogno di sapere quali thread stanno effettivamente consumando CPU, esegui il profiling con un profiler a campionamento — getState() non te lo dirà.
  • Dopo che tRunning.join() è tornato, il suo stato era TERMINATED. Puoi ancora interrogare il suo nome, ID e oggetto di stato, ma il thread è finito — isAlive() è false e start() lancerebbe un'eccezione. I thread sono monouso: quando uno termina, ne crei uno nuovo. (Questa è la principale motivazione per l'executor framework — un ExecutorService riutilizza lo stesso thread del sistema operativo per molti task.)

Cosa c'è dopo

Il prossimo capitolo, Metodi dei thread Java, esamina la superficie di metodi di Threadsleep, join, yield, interrupt, holdsLock e le utility statiche — con le insidie di ciascuno.

Esercitati

Pratica
Un thread è nello stato `BLOCKED`. Cosa sta aspettando?
Un thread è nello stato `BLOCKED`. Cosa sta aspettando?
Pratica
Un thread dump mostra un thread come `RUNNABLE` con `socketRead0` in cima al suo stack. Cosa sta effettivamente facendo?
Un thread dump mostra un thread come `RUNNABLE` con `socketRead0` in cima al suo stack. Cosa sta effettivamente facendo?
Pratica
Quale chiamata lascia un thread in `TIMED_WAITING` invece che in `WAITING`?
Quale chiamata lascia un thread in `TIMED_WAITING` invece che in `WAITING`?
Was this page helpful?