Жизненный цикл потока Java
Состояния потока Java — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — и правила переходов между ними.
У потока Java немного состояний — шесть, и все они являются значениями перечисления Thread.State. Но именно эти шесть составляют словарь дампов потоков, профайлеров и каждого расследования вопроса «почему моя программа зависла». Понимание того, что означает каждое состояние и какие переходы возможны, превращает дамп потоков из стены трассировок стека в диагноз.
Шесть состояний
// 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
}Допустимые переходы между состояниями образуют простой жизненный цикл:
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Каждое состояние соответствует чему-то видимому в дампе потоков. Разберём их по очереди.
NEW
Thread создан, но start() ещё не вызывался. Никакие ресурсы ОС не выделены; ничего не выполняется. Единственные переходы из этого состояния:
start()→RUNNABLE- Поток удаляется сборщиком мусора, так и не запустившись
Вызвать start() можно ровно один раз. Повторный вызов бросает IllegalThreadStateException.
RUNNABLE
«Поток жив и либо сейчас выполняется на процессоре, либо готов к выполнению.» Java объединяет состояния ОС «выполняется» и «готов к выполнению» в одно — по одному лишь Thread.State невозможно определить, потребляет ли поток CPU прямо сейчас. Планировщик ОС решает, какие из потоков RUNNABLE фактически получают ядро в каждый момент.
Поток также находится в состоянии RUNNABLE, когда заблокирован на I/O (InputStream.read, Socket.read, FileChannel.read). Это удивляет многих: поток «готов к выполнению» лишь в том смысле, что ничто в JVM его не блокирует. ОС знает, что поток ждёт диска; JVM — нет, поэтому отображает RUNNABLE. Если в дампе потоков вы видите поток в состоянии RUNNABLE, а верхний фрейм — socketRead0 или похожий, поток заблокирован на системном вызове, а не потребляет CPU.
RUNNABLE не означает «занят». Это наиболее часто неправильно интерпретируемое состояние в дампе потоков: поток, остановленный внутри блокирующего I/O вызова (socketRead0, FileInputStream.read), отображает RUNNABLE, хотя потребляет ноль CPU. Не делайте вывод об активности потока по его состоянию — читайте верхний фрейм стека или профилируйте с помощью профайлера.
BLOCKED
Поток стоит у дверей synchronized-блока, ожидая монитора. Другой поток его удерживает; этот поток стоит в очереди. Как только владелец освободит монитор, один из ожидающих получит блокировку и перейдёт обратно в RUNNABLE.
BLOCKED специфично для synchronized — встроенного механизма блокировок JVM. Код, ожидающий ReentrantLock, не показывает BLOCKED; он показывает WAITING (поскольку ReentrantLock реализован поверх LockSupport.park). Это небольшое, но важное различие при чтении дампов.
Типичная сигнатура состояния 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)Два важных элемента: на каком мониторе ждём (<0x000000076ab8e220>) и какой метод стоит у двери. Найдите в том же дампе строку - locked <0x000000076ab8e220> — и вы обнаружите поток, который его удерживает.
WAITING
Поток решил ждать бесконечно. В это состояние его переводят три вещи:
Object.wait()— освобождает монитор и паркуется, пока кто-то не вызоветnotify/notifyAll.Thread.join()— без таймаута паркуется, пока целевой поток не завершится.LockSupport.park()— примитив, на котором построеныReentrantLock.lock(),await(),BlockingQueue.take()и весьjava.util.concurrent.
Поток в состоянии WAITING практически не потребляет ресурсов, кроме своего стека. Он не выйдет из состояния, пока кто-то другой что-то не сделает — вызовет notify, завершит join-цель или вызовет LockSupport.unpark. Если никто его не разбудит, он будет ждать вечно. Именно так выглядят тихие взаимоблокировки в дампе: два потока, оба в WAITING, оба удерживающие то, что нужно другому.
TIMED_WAITING
То же самое, что WAITING, но с дедлайном. Поток сам проснётся по истечении таймаута, даже если ничего не произойдёт. В это состояние переводят:
Thread.sleep(ms)Object.wait(ms)Thread.join(ms)LockSupport.parkNanos(...),LockSupport.parkUntil(...)BlockingQueue.poll(timeout, unit),Future.get(timeout, unit)и т. д.
Если поток стабильно находится в TIMED_WAITING в течение указанного вами времени — это не баг. Если он остаётся там дольше таймаута, значит его снова поставили в ожидание — кто-то вызвал wait(1000) в цикле, или очередь по-прежнему пуста.
TERMINATED
run() вернул управление (нормально или через исключение). Поток завершён; его нельзя перезапустить. t.isAlive() возвращает false. Вы можете считать его имя и ID для целей логирования и отладки, но сам поток закончил работу.
Считывание состояния из собственного кода
Thread.State доступен для публичного запроса, но значение является снимком — оно может измениться между вызовом и использованием. В продакшне вы почти никогда не ветвитесь по нему; вы используете его для логирования и диагностики. JVM также предоставляет ThreadMXBean для полных дампов потоков, что отображают большинство JMX-дашбордов.
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Практический пример: наблюдение всех состояний
Приведённая ниже программа создаёт потоки, каждый из которых застревает в определённом состоянии, а затем выводит их текущее состояние.
Что следует вынести из запуска:
- Каждое из шести состояний было достигнуто кодом в одной программе.
NEWиTERMINATED— граничные случаи; средние четыре (RUNNABLE,BLOCKED,WAITING,TIMED_WAITING) — те, что вы увидите в реальном дампе потоков. blocked-threadотобразилBLOCKED, потому что holder владелsynchronized-монитором. Если бы мы использовалиReentrantLock, тот же путь кода показал быWAITING(посколькуLock.lock()паркует черезLockSupport). Имя состояния говорит вам какой тип ожидания, а не просто «этот поток завис».waiting-threadостался бы вWAITINGнавсегда, если быmainне вызвалcond.notify(). СостояниеWAITINGне имеет таймаута — кто-то другой должен его разбудить. Именно так пропущенныйnotifyприводит к взаимоблокировке, о которой никогда не сообщает ни одно исключение.- Поток, сжигающий CPU, отображал
RUNNABLEнезависимо от того, действительно ли он выполнялся на ядре или просто стоял в очереди выполнения в ожидании него. JVM не различает «выполняется» и «готов»; это делает ОС. Если нужно знать, какие потоки реально потребляют CPU, используйте профайлер с выборкой —getState()этого не скажет. - После возврата
tRunning.join()его состояние сталоTERMINATED. Вы по-прежнему можете запросить его имя, ID и объект состояния, но поток исчез —isAlive()возвращаетfalse, аstart()выбросит исключение. Потоки одноразовые: когда поток завершается, создаётся новый. (Это главная мотивация для фреймворка executor —ExecutorServiceповторно использует один и тот же поток ОС для множества задач.)
Что дальше
Следующая глава, Методы потока Java, рассматривает API класса Thread — sleep, join, yield, interrupt, holdsLock и статические утилиты — с подводными камнями для каждого из них.