Ciclo de Vida de Threads Java
Os estados de uma thread Java — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — e como eles mudam.
Uma thread Java não tem muitos estados — são seis, todos eles valores do enum Thread.State. Mas esses seis são o vocabulário de dumps de threads, profilers e de toda investigação do tipo "por que meu programa travou" que você já fará. Saber o que cada estado significa e quais transições são possíveis é o que transforma um dump de thread de uma parede de stack traces em um diagnóstico.
Os seis estados
// 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
}As transições permitidas entre eles formam um ciclo de vida simples:
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
TERMINATEDCada estado corresponde a algo visível em um dump de thread. Vamos percorrê-los.
NEW
Uma Thread que você construiu mas nunca chamou start(). Nenhum recurso de SO foi alocado; nada está em execução. As únicas transições possíveis são:
start()→RUNNABLE- A thread é coletada pelo garbage collector sem nunca ter sido executada
Você pode chamar start() exatamente uma vez. Uma segunda chamada lança IllegalThreadStateException.
RUNNABLE
"A thread está viva e está atualmente em execução em uma CPU ou pronta para executar." O Java colapsa os estados "running" e "runnable" do SO em um único estado — não há como saber apenas pelo Thread.State se a thread está atualmente consumindo CPU. O escalonador do SO decide quais threads RUNNABLE realmente obtêm um núcleo a cada momento.
Uma thread RUNNABLE também é o estado em que uma thread se encontra quando está bloqueada em I/O (InputStream.read, Socket.read, FileChannel.read). Isso surpreende as pessoas: a thread está "pronta para executar" apenas no sentido de que nada na JVM a está bloqueando. O SO sabe que a thread está aguardando o disco; a JVM não sabe, então reporta RUNNABLE. Se você vir um dump de thread onde uma thread é RUNNABLE e seu frame do topo é socketRead0 ou similar, a thread está bloqueada em uma syscall — não consumindo CPU.
RUNNABLE não significa "ocupada." É o estado mais mal interpretado em um dump de thread: uma thread estacionada dentro de uma chamada de I/O bloqueante (socketRead0, FileInputStream.read) reporta RUNNABLE mesmo que esteja usando zero CPU. Não conclua que uma thread está ativa com base no seu estado — leia seu frame de topo do stack, ou faça amostragem com um profiler.
BLOCKED
A thread está na porta de um bloco synchronized aguardando o lock do monitor. Alguma outra thread o possui; esta entrou na fila. Assim que o detentor liberar, um dos esperadores ganha o lock e faz a transição de volta para RUNNABLE.
BLOCKED é específico para synchronized — o mecanismo de lock intrínseco que a JVM incorpora. Código aguardando em um ReentrantLock não mostra BLOCKED; mostra WAITING (porque ReentrantLock é implementado em cima de LockSupport.park). Essa é uma distinção pequena, mas importante ao ler dumps.
A assinatura clássica de dump de thread para 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)Duas informações: em qual monitor você está aguardando (<0x000000076ab8e220>) e qual método está na porta. Pesquise no mesmo dump por - locked <0x000000076ab8e220> e você terá encontrado a thread que o possui.
WAITING
A thread escolheu aguardar indefinidamente. Três coisas colocam uma thread aqui:
Object.wait()— libera o monitor e estaciona até que alguém chamenotify/notifyAll.Thread.join()— sem timeout, estaciona até que a thread alvo termine.LockSupport.park()— o primitivo sobre o qualReentrantLock.lock(),await(),BlockingQueue.take(), e todo ojava.util.concurrentsão construídos.
Uma thread WAITING usa essencialmente nenhum recurso além de sua stack. Ela não fará transição até que alguém mais faça algo — um notify, o término de um alvo de join, um LockSupport.unpark. Se nada nunca a acionar, ela fica lá para sempre. É assim que deadlocks silenciosos parecem em um dump: duas threads, ambas em WAITING, ambas segurando o que a outra quer.
TIMED_WAITING
A mesma ideia que WAITING, mas com um prazo. A thread se acordará quando o timeout expirar, mesmo que nada mais aconteça. Coisas que produzem TIMED_WAITING:
Thread.sleep(ms)Object.wait(ms)Thread.join(ms)LockSupport.parkNanos(...),LockSupport.parkUntil(...)BlockingQueue.poll(timeout, unit),Future.get(timeout, unit), etc.
Se uma thread está confiavelmente presa em TIMED_WAITING pela duração que você especificou, isso não é um bug. Se ela permanecer lá além do timeout, ela foi reestacionada — alguém chamou wait(1000) em um loop ou a fila ainda está vazia.
TERMINATED
run() retornou (normalmente ou por exceção). A thread terminou; não pode ser reiniciada. t.isAlive() retorna false. Você ainda pode ler seu nome e ID para fins de log/depuração, mas a thread em si está finalizada.
Lendo o estado no seu próprio código
Thread.State é consultável publicamente, mas o valor é um snapshot — ele pode mudar entre a chamada e o uso que você fizer dele. Em produção, você quase nunca ramifica nele; você o usa para logging e diagnóstico. A JVM também expõe ThreadMXBean para dumps completos de threads, que é o que a maioria dos dashboards JMX exibe.
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()); // TERMINATEDUm exemplo trabalhado: observe cada estado
O programa abaixo cria threads que ficam presas em estados diferentes, depois imprime em qual estado estão.
O que extrair da execução:
- Cada um dos seis estados era alcançável por código no mesmo programa.
NEWeTERMINATEDsão os casos limites; os quatro do meio (RUNNABLE,BLOCKED,WAITING,TIMED_WAITING) são os que você verá em um dump de thread real. - A
blocked-threadreportouBLOCKEDporque o holder possuía o monitorsynchronized. Se tivéssemos usado umReentrantLockno lugar, o mesmo caminho de código teria reportadoWAITING(poisLock.lock()estaciona viaLockSupport). O nome do estado diz que tipo de espera, não apenas "esta thread está presa." - A
waiting-threadteria ficado emWAITINGpara sempre semainnão tivesse chamadocond.notify(). O estadoWAITINGnão tem timeout — alguém mais precisa acordá-la. É exatamente assim que umnotifyperdido produz um deadlock que nenhuma exceção jamais relata. - A thread que queimava CPU reportou
RUNNABLEestivesse ela de fato em execução em um núcleo ou meramente sentada na fila de execução aguardando por um. A JVM não distingue "em execução" de "pronta"; o SO distingue. Se você precisar saber quais threads estão realmente consumindo CPU, use um profiler de amostragem —getState()não lhe dirá. - Após
tRunning.join()retornar, seu estado eraTERMINATED. Você ainda pode consultar seu nome, ID e objeto de estado, mas a thread acabou —isAlive()éfalseestart()lançaria uma exceção. Threads são de uso único: quando uma termina, você cria uma nova. (Esta é a principal motivação para o framework executor — umExecutorServicereutiliza a mesma thread do SO para muitas tarefas.)
O que vem a seguir
O próximo capítulo, Métodos de Thread Java, percorre a superfície de métodos de Thread — sleep, join, yield, interrupt, holdsLock, e os utilitários estáticos — com as armadilhas de cada um.