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()).
  • Стежте за станами потоків під час налагодження — це допоможе знайти зависання та взаємні блокування.
  • Не забувайте про 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() — зло. Не використовуйте їх, навіть якщо дуже хочеться «швидко все полагодити».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ