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
- Запустите программу — она зависнет.
- Получите thread dump (jstack или через VisualVM).
- Найдите «Thread-1» и «Thread-2» — увидите, что каждый держит один lock и ждёт другой.
- В конце дампа будет секция «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() и анализируйте состояние общих переменных.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ