1. Основные состояния потока в Java
В Java поток — это не просто «запущен» или «остановлен». У него есть целый жизненный цикл, и на каждом этапе поток ведёт себя по-разному. Понимание этих этапов — ключ к написанию стабильных многопоточных приложений и к отладке странных «зависаний» и «неожиданных завершений».
Какие бывают состояния?
Java определяет следующие основные состояния потока (они перечислены в перечислении Thread.State):
| Состояние | Описание |
|---|---|
|
Поток создан, но ещё не запущен (new Thread(...), но start() не вызывался) |
|
Поток готов к исполнению или исполняется прямо сейчас |
|
Поток ожидает освобождения монитора (заблокирован на synchronized-блоке) |
|
Поток ожидает, пока другой поток его «разбудит» (например, через Object.wait()) |
|
Поток ожидает с тайм-аутом (например, Thread.sleep(1000), wait(1000), join(1000)) |
|
Поток завершил выполнение |
Интересный факт:
В старых книгах и статьях вы можете встретить другие названия или чуть другие схемы. Но с Java 5+ эти состояния считаются стандартом.
Визуальная схема жизненного цикла потока
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
NEWRUNNABLETERMINATEDThread 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
RUNNABLETIMED_WAITINGRUNNABLETERMINATEDThread 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()
RUNNABLEWAITINGThread 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
BLOCKEDObject 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, если поток запущен и ещё не завершился (
— это уже false).TERMINATED - 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() — зло. Не используйте их, даже если очень хочется «быстро всё починить».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ