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()).
- Стежте за станами потоків під час налагодження — це допоможе знайти зависання та взаємні блокування.
- Не забувайте про 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() — зло. Не використовуйте їх, навіть якщо дуже хочеться «швидко все полагодити».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ