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. Best practices диагностики

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

Держите блокировки как можно меньшее время.

Плохой пример:

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() и таймаутом: если не удалось захватить все lock’и — отпустите и попробуйте снова.

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
Задача
JAVA 25 SELF, 53 уровень, 4 лекция
Недоступна
Детектив внутри программы: Определение состояния текущего потока 🕵️‍♂️
Детектив внутри программы: Определение состояния текущего потока 🕵️‍♂️
1
Задача
JAVA 25 SELF, 53 уровень, 4 лекция
Недоступна
Доступ к Секретной Комнате: Логирование входа и выхода из критической секции 🚪
Доступ к Секретной Комнате: Логирование входа и выхода из критической секции 🚪
1
Опрос
Проблемы многопоточности, 53 уровень, 4 лекция
Недоступен
Проблемы многопоточности
Проблемы многопоточности
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ