JavaRush /Курсы /JAVA 25 SELF /Состояния и жизненный цикл потока

Состояния и жизненный цикл потока

JAVA 25 SELF
51 уровень , 2 лекция
Открыта

1. Основные состояния потока в Java

В Java поток — это не просто «запущен» или «остановлен». У него есть целый жизненный цикл, и на каждом этапе поток ведёт себя по-разному. Понимание этих этапов — ключ к написанию стабильных многопоточных приложений и к отладке странных «зависаний» и «неожиданных завершений».

Какие бывают состояния?

Java определяет следующие основные состояния потока (они перечислены в перечислении Thread.State):

Состояние Описание
NEW
Поток создан, но ещё не запущен (new Thread(...), но start() не вызывался)
RUNNABLE
Поток готов к исполнению или исполняется прямо сейчас
BLOCKED
Поток ожидает освобождения монитора (заблокирован на synchronized-блоке)
WAITING
Поток ожидает, пока другой поток его «разбудит» (например, через Object.wait())
TIMED_WAITING
Поток ожидает с тайм-аутом (например, Thread.sleep(1000), wait(1000), join(1000))
TERMINATED
Поток завершил выполнение

Интересный факт:
В старых книгах и статьях вы можете встретить другие названия или чуть другие схемы. Но с Java 5+ эти состояния считаются стандартом.

Визуальная схема жизненного цикла потока

stateDiagram-v2 [*] --> NEW NEW --> RUNNABLE: start() RUNNABLE --> BLOCKED: попытка войти в synchronized, но монитор занят BLOCKED --> RUNNABLE: монитор освобождён RUNNABLE --> WAITING: wait(), join() RUNNABLE --> TIMED_WAITING: sleep(), wait(timeout), join(timeout) WAITING --> RUNNABLE: notify()/notifyAll(), join() завершился TIMED_WAITING --> RUNNABLE: таймаут истёк / notify()/notifyAll() RUNNABLE --> TERMINATED: run() завершился WAITING --> TERMINATED: run() завершился (редко) TIMED_WAITING --> TERMINATED: run() завершился (редко)

2. Методы управления потоком

Сон: Thread.sleep(long millis)

Иногда потоку нужно «поспать», чтобы не мешать другим или подождать события. Метод Thread.sleep(ms) переводит поток в состояние

TIMED_WAITING
на заданное количество миллисекунд.

System.out.println("Поток засыпает на 2 секунды...");
Thread.sleep(2000); // Засыпаем на 2 секунды
System.out.println("Поток проснулся!");
  • После окончания сна поток возвращается в состояние
    RUNNABLE
    (готов к работе).
  • Если поток прерван во время сна, выбрасывается InterruptedException.

Ожидание завершения другого потока: join()

Иногда нужно не просто запустить поток, а дождаться, пока он закончит работу. Для этого в Java есть метод join():

Thread t = new Thread(() -> {
    System.out.println("Работаю...");
    try { Thread.sleep(1000); } catch (InterruptedException e) {}
    System.out.println("Готово!");
});
t.start();
System.out.println("Жду завершения потока t...");
t.join(); // Текущий поток (например, main) ждёт t
System.out.println("Поток t завершился!");

Здесь главный поток начинает ждать, как только вызывается join(). Всё это время он находится в состоянии

WAITING
, пока поток t не закончит выполнение. Есть и вариант с таймаутом — join(long millis). В таком случае поток ждёт ограниченное время, и его состояние будет
TIMED_WAITING
.

Прерывание потока: interrupt()

Бывает ситуация, когда нужно вежливо попросить поток закончить работу, например, если пользователь нажал кнопку «Отмена». Для этого используется метод interrupt():

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Работаем...
    }
    System.out.println("Поток завершён по прерыванию!");
});
t.start();
// ... через время:
t.interrupt(); // Сигнал потоку: "пора остановиться"

Важно понимать, что вызов interrupt() не убивает поток мгновенно. Он лишь выставляет специальный флаг. Сам поток должен время от времени проверять этот флаг через isInterrupted() и завершаться самостоятельно. Если поток в это время находится в состоянии сна (sleep()) или ожидания (wait()), то он не просто увидит флаг, а сразу получит исключение InterruptedException. Именно так в Java устроен «корректный» способ остановки потоков: программа не прерывает их силой, а уведомляет, что пора завершаться.

3. Примеры переходов между состояниями

Давайте рассмотрим на примерах, как поток «путешествует» по своим состояниям.

Пример 1:
NEW
RUNNABLE
TERMINATED

Thread t = new Thread(() -> System.out.println("Привет!"));
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (или TERMINATED, если поток очень быстрый)
t.join();
System.out.println(t.getState()); // TERMINATED

Пример 2:
RUNNABLE
TIMED_WAITING
RUNNABLE
TERMINATED

Thread t = new Thread(() -> {
    try {
        System.out.println("Засыпаю...");
        Thread.sleep(1000); // TIMED_WAITING
        System.out.println("Проснулся!");
    } catch (InterruptedException e) {
        System.out.println("Поток прерван!");
    }
});
t.start();

Пример 3:
RUNNABLE
WAITING
с join()

Thread t1 = new Thread(() -> {
    try { Thread.sleep(500); } catch (InterruptedException ignored) {}
    System.out.println("t1 завершён");
});
Thread t2 = new Thread(() -> {
    try {
        t1.join(); // t2 ждёт t1, находится в WAITING
        System.out.println("t2 дождался t1");
    } catch (InterruptedException ignored) {}
});
t1.start();
t2.start();

Пример 4:
BLOCKED

Object lock = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lock) {
        try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
        System.out.println("t1 вышел из блока synchronized");
    }
});
Thread t2 = new Thread(() -> {
    synchronized (lock) {
        System.out.println("t2 вошёл в synchronized");
    }
});
t1.start();
Thread.sleep(100); // Дадим t1 захватить lock
t2.start();
Thread.sleep(100); // Дадим t2 попытаться войти в synchronized
System.out.println("Состояние t2: " + t2.getState()); // BLOCKED

4. Как узнать состояние потока? Методы isAlive() и getState()

  • isAlive() — возвращает true, если поток запущен и ещё не завершился (
    TERMINATED
    — это уже false).
  • getState() — возвращает текущее состояние потока (значение из перечисления Thread.State).
Thread t = new Thread(() -> {});
System.out.println(t.isAlive()); // false (NEW)
t.start();
System.out.println(t.isAlive()); // true (RUNNABLE/WAITING/...)
t.join();
System.out.println(t.isAlive()); // false (TERMINATED)

5. Почему нельзя «убить» поток напрямую и другие практические советы

Нет метода «kill»!

В Java нет метода, который бы позволил «убить» поток по команде. Почему? Потому что это небезопасно: если поток держит какой-то ресурс (файл, соединение, блокировку), его принудительное уничтожение может оставить систему в неконсистентном состоянии.

Устаревшие методы: stop(), suspend(), resume()

В древних версиях Java были методы stop(), suspend(), resume(). Сейчас они помечены как @Deprecated, а их использование категорически не рекомендуется. Почему?

  • stop() может убить поток в любой момент, оставив данные в неконсистентном состоянии.
  • suspend() может «заморозить» поток, который держит блокировку, и тогда вся программа повиснет.
  • resume() иногда не может «разморозить» поток, если тот уже завершился.

Современный подход:
Поток должен сам корректно завершаться, реагируя на флаг прерывания (isInterrupted()) или на другие сигналы.

Лучшие практики

  • Не вызывайте методы, которые помечены как устаревшие.
  • Используйте флаг прерывания для остановки потока (interrupt() и проверка isInterrupted()).
  • Следите за состояниями потоков при отладке — это поможет найти зависания и deadlock'и.
  • Не забывайте про join(), если нужно дождаться завершения работы потока.

6. Типичные ошибки при работе с жизненным циклом потока

Ошибка №1: Запускать поток повторно.
В Java поток можно запустить только один раз. Если вызвать start() второй раз — получите IllegalThreadStateException. Если вам нужно повторить задачу — создайте новый объект Thread.

Thread t = new Thread(() -> {});
t.start();
t.start(); // Бросит исключение!

Ошибка №2: Путать run() и start().
Вызов run() напрямую не запускает код в новом потоке — он выполняется в текущем (например, в main). Только start() действительно запускает новый поток.

Ошибка №3: Не обрабатывать InterruptedException.
Если поток спит или ждёт, и его прервали, будет выброшено InterruptedException. Если его проигнорировать, поток может «уснуть навсегда» или завершиться неожиданно.

Ошибка №4: Не проверять состояние потока.
Иногда программа зависает, потому что один поток ждёт другой, который уже завершился или никогда не начнёт работу. Используйте getState() и isAlive() для диагностики.

Ошибка №5: Использовать устаревшие методы управления потоком.
Методы stop(), suspend(), resume() — зло. Не используйте их, даже если очень хочется «быстро всё починить».

1
Задача
JAVA 25 SELF, 51 уровень, 2 лекция
Недоступна
Контроль завершения космической миссии 🛰️
Контроль завершения космической миссии 🛰️
1
Задача
JAVA 25 SELF, 51 уровень, 2 лекция
Недоступна
Синхронизация работы двух сотрудников 🤝
Синхронизация работы двух сотрудников 🤝
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ