W3docs

Жизненный цикл потока 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

Практический пример: наблюдение всех состояний

Приведённая ниже программа создаёт потоки, каждый из которых застревает в определённом состоянии, а затем выводит их текущее состояние.

java— editable, runs on the server

Что следует вынести из запуска:

  • Каждое из шести состояний было достигнуто кодом в одной программе. 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() выбросит исключение. Потоки одноразовые: когда поток завершается, создаётся новый. (Это главная мотивация для фреймворка executorExecutorService повторно использует один и тот же поток ОС для множества задач.)

Что дальше

Следующая глава, Методы потока Java, рассматривает API класса Threadsleep, join, yield, interrupt, holdsLock и статические утилиты — с подводными камнями для каждого из них.

Практика

Практика
Поток находится в состоянии `BLOCKED`. Чего он ждёт?
Поток находится в состоянии `BLOCKED`. Чего он ждёт?
Практика
В дампе потоков поток отображается как `RUNNABLE`, а верхний фрейм стека — `socketRead0`. Что он реально делает?
В дампе потоков поток отображается как `RUNNABLE`, а верхний фрейм стека — `socketRead0`. Что он реально делает?
Практика
Какой вызов переводит поток в `TIMED_WAITING`, а не в `WAITING`?
Какой вызов переводит поток в `TIMED_WAITING`, а не в `WAITING`?
Was this page helpful?