JavaRush /Курси /JAVA 25 SELF /Діагностика та налагодження багатопотокових програм

Діагностика та налагодження багатопотокових програм

JAVA 25 SELF
Рівень 53 , Лекція 4
Відкрита

1. Thread Dump і аналіз станів потоків

Thread Dump (дамп потоків) — це знімок стану всіх потоків у застосунку на певний момент часу. Це як групове фото всіх ваших потоків: хто чим зайнятий, хто де застряг, хто на кого чекає. Thread Dump — основний інструмент для пошуку deadlock, livelock та інших загадкових зависань.

Як отримати Thread Dump?

Через термінал (jstack):

Якщо у вас є PID процесу Java, виконайте:

jstack <PID>

Команда виведе у консоль стан усіх потоків із зазначенням, хто в якому стані перебуває та які монітори (блокування) утримує.

Через IDE (IntelliJ IDEA):
У меню «Run» → «Show Running List» → оберіть процес → «Thread Dump».

Через VisualVM або JConsole:
Відкрийте процес, знайдіть вкладку «Threads» і зробіть знімок стану.

Приклад Thread Dump

Фрагмент дампу:

"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001e0c7800 nid=0x1a48 waiting for monitor entry [0x000000001f00f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:25)
    - waiting to lock <0x00000000d6d6baf8> (a java.lang.Object)
    - locked <0x00000000d6d6bb08> (a java.lang.Object)

Тут видно, що потік «Thread-1» заблокований (BLOCKED), тримає один монітор, але очікує на інший. Якщо ви бачите кілька таких потоків, які тримають ресурс A і очікують на B, а інший потік тримає B і очікує на A — це класичний deadlock.

Стани потоків

Статус Опис
RUNNABLE Потік виконується або готовий до виконання
BLOCKED Очікує на захоплення монітора (блокування)
WAITING Чекає на notify()/notifyAll() (наприклад, викликано wait())
TIMED_WAITING Чекає з тайм-аутом (наприклад, sleep, wait(timeout))
TERMINATED Потік завершений

Важливо: статус RUNNABLE не завжди означає, що потік саме зараз виконується — він лише готовий до виконання (планувальник JVM може не запускати його негайно).

Як зрозуміти, що у вас deadlock?

У дампі кілька потоків у стані BLOCKED, кожен очікує на монітор, який утримує інший потік із цього ж набору.

Наприкінці дампу jstack, як правило, повідомляє:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00000000d6d6baf8 (object 0x00000000d6d6baf8, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x00000000d6d6bb08 (object 0x00000000d6d6bb08, a java.lang.Object),
  which is held by "Thread-1"

Якщо потоки тривалий час перебувають у BLOCKED або WAITING — це привід для розслідування.

2. Моніторинг і профілювання потоків

VisualVM
VisualVM — безкоштовний інструмент, що входить до складу більшості JDK. Дозволяє під’єднатися до процесу, переглянути стан потоків, зробити Thread Dump, побачити навантаження на CPU, активні та «завислі» потоки.

Вкладка Threads: показує, скільки потоків створено, їхні стани та історію активності.

Thread Dump: кнопка «Thread Dump» робить знімок, аналогічний до jstack.

Java Mission Control і Flight Recorder

Java Mission Control (JMC): потужний інструмент аналізу роботи JVM у реальному часі. Допомагає досліджувати блокування, час виконання, виділення пам’яті, затримки.

Java Flight Recorder (JFR): вбудований профілювальник JVM, що збирає події про потоки, блокування, паузи тощо.

Приклад: моніторинг блокувань

У VisualVM або JMC можна побачити, що:

  • Потік «A» заблокований на об’єкті X.
  • Потік «B» тримає об’єкт X, але чекає на об’єкт Y.
  • Потік «C» тримає об’єкт Y, але чекає на об’єкт X.

Це класичне кругове блокування (deadlock).

Як використовувати ці інструменти на практиці?

  • Запускайте застосунок із ключем -XX:+FlightRecorder (або просто використовуйте JDK 11+).
  • Відкрийте JMC, під’єднайтеся до процесу, почніть запис (start recording).
  • Аналізуйте «вузькі місця», тривалі блокування та конкуренцію між потоками.

3. Логування і трасування

У багатопотокових програмах налагодження «на око» часто болісне. Логуйте вхід/вихід із критичних секцій (synchronized-блоків), операції зі спільними змінними, очікування та пробудження потоків — так ви зрозумієте, хто і коли захопив або відпустив ресурс.

Як логувати?

  • Використовуйте стандартні засоби: java.util.logging, SLF4J, Log4j.
  • Логуйте ім’я потоку: Thread.currentThread().getName().
  • Логуйте час та ідентифікатори потоків.
  • Логуйте події захоплення/звільнення блокувань.

Приклад логування

synchronized(lock) {
    System.out.println(Thread.currentThread().getName() + " захопив lock");
    // критична секція
    System.out.println(Thread.currentThread().getName() + " виходить із lock");
}

Використання імен потоків

Призначайте потокам осмислені імена!

Thread t = new Thread(runnable, "MyWorker-1");

Приклад трасування за допомогою логера

import java.util.logging.Logger;

public class Example {
    private static final Logger logger = Logger.getLogger(Example.class.getName());

    public void doWork() {
        logger.info(Thread.currentThread().getName() + " розпочав роботу");
        synchronized (this) {
            logger.info(Thread.currentThread().getName() + " увійшов у synchronized");
            // ...
        }
        logger.info(Thread.currentThread().getName() + " завершив роботу");
    }
}

4. Найкращі практики діагностики

Мінімізуйте область блокувань

Тримайте блокування якнайменший час.

Поганий приклад:

synchronized(lock) {
    // тривале введення-виведення
    // складні обчислення
    // звернення до БД
    // ... і лише потім робота зі спільними даними
}

Хороший приклад:

// поза synchronized: тривале введення-виведення, обчислення

synchronized(lock) {
    // лише робота зі спільними даними
}

Використовуйте імена потоків

Осмислені імена потоків заощаджують час на аналізі дампів і логів.

Пишіть тести для багатопоточності

Використовуйте JUnit + CountDownLatch, щоб моделювати конкурентні сценарії.

CountDownLatch latch = new CountDownLatch(2);
Runnable task = () -> {
    // ...
    latch.countDown();
};
new Thread(task, "Worker-1").start();
new Thread(task, "Worker-2").start();
latch.await(); // чекаємо завершення обох потоків

Використовуйте try-finally для ReentrantLock

Lock lock = new ReentrantLock();
lock.lock();
try {
    // критична секція
} finally {
    lock.unlock();
}

Так ви не забудете звільнити блокування навіть у разі винятку. Щоб уникнути взаємних блокувань, використовуйте tryLock() із тайм-аутом.

Документуйте, навіщо потрібна синхронізація

Коментарі на кшталт «Тут потрібен synchronized, тому що…» допоможуть з часом зрозуміти задум.

5. Практика: аналіз deadlock у тестовій програмі

Приклад коду з deadlock

public class DeadlockDemo {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("Thread-1: захопив lockA");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                synchronized (lockB) {
                    System.out.println("Thread-1: захопив lockB");
                }
            }
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("Thread-2: захопив lockB");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                synchronized (lockA) {
                    System.out.println("Thread-2: захопив lockA");
                }
            }
        }, "Thread-2");

        t1.start();
        t2.start();
    }
}

Як спіймати deadlock

  1. Запустіть програму — вона зависне.
  2. Отримайте Thread Dump (jstack або через VisualVM).
  3. Знайдіть «Thread-1» і «Thread-2» — побачите, що кожен тримає один lock і чекає на інший.
  4. У кінці дампу буде секція «Found one Java-level deadlock».

Як усунути

  • Захоплюйте блокування завжди в одному й тому самому порядку.
  • Використовуйте ReentrantLock із tryLock() і тайм-аутом: якщо не вдалося захопити усі блокування — відпустіть і спробуйте знову.

6. Типові помилки під час діагностики багатопотокових програм

Помилка № 1: Невміння читати Thread Dump. Розробники-початківці лякаються дампу: «Що за дивні стек-трейси та статуси?» Насправді достатньо знати основні стани й шукати BLOCKED/WAITING, щоб спростити аналіз.

Помилка № 2: Ігнорування імен потоків. Без осмислених імен розбиратися в дампі — як шукати голку в копиці сіна. Не лінуйтеся призначати імена!

Помилка № 3: Надто великі synchronized-блоки. Якщо ви синхронізуєте великі шматки коду, потоки частіше блокуватимуться один об одного — це видно за частими BLOCKED у дампі.

Помилка № 4: Плутанина між RUNNABLE і реально працюючим потоком. RUNNABLE — не завжди «біжить» на процесорі. Планувальник JVM сам вирішує, кого запускати.

Помилка № 5: Невикористання інструментів моніторингу. Багато хто не знає про VisualVM, JMC, Flight Recorder і мучиться з println. Використовуйте інструменти — вони значно спрощують життя.

Помилка № 6: Відсутність логування критичних операцій. Без логів зрозуміти, хто і коли захопив/відпустив блокування, майже неможливо.

Помилка № 7: Спроби «на око» спіймати гонки даних. Гонки проявляються не завжди й не відразу — використовуйте тести з CountDownLatch, провокуйте конкуренцію через Thread.yield() і аналізуйте стан спільних змінних.

1
Опитування
Проблеми багатопоточності, рівень 53, лекція 4
Недоступний
Проблеми багатопоточності
Проблеми багатопоточності
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ