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
TERMINATEDOgni 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.
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 chiamanotify/notifyAll.Thread.join()— senza timeout, si parcheggia finché il thread target termina.LockSupport.park()— la primitiva su cui sono costruitiReentrantLock.lock(),await(),BlockingQueue.take()e tuttojava.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()); // TERMINATEDUn 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.
Cosa ricavare dall'esecuzione:
- Ognuno dei sei stati era raggiungibile dal codice nello stesso programma.
NEWeTERMINATEDsono i casi limite; i quattro centrali (RUNNABLE,BLOCKED,WAITING,TIMED_WAITING) sono quelli che vedrai in un thread dump reale. - Il
blocked-threadha riportatoBLOCKEDperché il holder possedeva il monitorsynchronized. Se avessimo usato unReentrantLockal posto, lo stesso percorso di codice avrebbe riportatoWAITING(poichéLock.lock()si parcheggia tramiteLockSupport). Il nome dello stato ti dice che tipo di attesa, non solo "questo thread è bloccato." - Il
waiting-threadsarebbe rimasto inWAITINGper sempre semainnon avesse chiamatocond.notify(). Lo statoWAITINGnon ha timeout — qualcun altro deve svegliarlo. È esattamente così che unanotifymancata produce un deadlock che nessuna eccezione riporta mai. - Il thread che consumava CPU ha riportato
RUNNABLEsia 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 eraTERMINATED. Puoi ancora interrogare il suo nome, ID e oggetto di stato, ma il thread è finito —isAlive()èfalseestart()lancerebbe un'eccezione. I thread sono monouso: quando uno termina, ne crei uno nuovo. (Questa è la principale motivazione per l'executor framework — unExecutorServiceriutilizza 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 Thread — sleep, join, yield, interrupt, holdsLock e le utility statiche — con le insidie di ciascuno.