С появлением виртуальных потоков в Java благодаря Project Loom, параллельное программирование стало проще, а производительность — выше. Однако за этой простотой кроются новые вызовы для инструментов отладки и анализа. Как читать тред-дампы, если их теперь тысячи — или миллионы? Какие средства реально помогают найти взаимные блокировки и аномалии в асинхронном коде? Рассмотрим в новом переводе от команды Spring АйО.


Если вы следите за развитием виртуальных потоков в Java, то уже знаете, что эта горячая новинка значительно повышает эффективность использования аппаратных ресурсов при параллельной обработке операций, в которых доминирует I/O. Виртуальные потоки позволяют делать несколько одновременных операций ввода-вывода одним потоком операционной системы без его блокировки. Новизна подхода заключается в том, что он требует минимальных изменений в кодовой базе, поскольку предоставляет легковесный примитив многопоточности, совместимый с существующими API.

Это, безусловно, отличная новость для Java-разработчиков. Ранее для достижения аналогичных результатов приходилось писать сложные пайплайны на основе колбэков или использовать реактивные Java-фреймворки, которые нельзя было назвать простыми.

За редкими исключениями, виртуальные потоки в Java действительно так хороши, как о них говорят. API прост и знаком, а пропускная способность увеличивается на порядки. Там, где серверы ранее справлялись лишь с несколькими сотнями запросов, они могут обрабатывать миллионы.

Что это означает для остальной экосистемы?

Состояние инструментов


Виртуальные потоки создают настоящий вызов существующим Java-инструментам. Ваш визуальный просмотрщик тред-дампов или отладчик теперь может показать миллионы строк, и даже простое отображение всей этой информации может оказаться затруднительным. Но даже если это удаётся, остаётся проблема UX: как пользователю ��риентироваться в таком объёме данных?

Или возьмём, к примеру, классическую проблему отладки асинхронного кода: планировщик и реальная полезная работа обычно выполняются в разных потоках, в результате чего стек трейс рабочего потока (worker-а) оказывается логически неполным. Задача запускается в одном потоке, а сбой происходит в другом, и в итоге стектрейс ошибки не содержит частей из которых можно понять, как задача попала в планировщик. А это может быть ключом к пониманию причины сбоя.

Хотя экосистема Java в целом успешно адаптируется к изменениям, некоторые пробелы всё же остаются. Каждая из таких задач многогранна и вполне заслуживает отдельной статьи. В оставшейся части этой публикации мы подробнее рассмотрим одну из них — тред-дампы и то, как эффективно ими пользоваться.

Тред-дампы


Когда речь заходит об инструментах и многопоточности, первое, что приходит на ум, — это тред-дампы!

Взаимная блокировка? Завис UI? Thread explosion или утечка? При расследовании любой из этих проблем тред-дамп обычно становится отправной точкой. Простые, но мощные, тред-дампы остаются одним из лучших инструментов для диагностики проблем многопоточности. В лучшем случае они укажут точно, где находится проблема. В худшем — дадут хотя бы начальную точку для анализа.

Инструмент тред-дампа фиксирует состояние приложения в конкретный момент времени и создаёт структурированный текстовый отчёт по каждому потоку в приложении. Этот отчёт можно просматривать как в виде обычного текста, так и через специализированный просмотрщик дампов, например, тот, что встроен в IntelliJ IDEA или в Open IDE:

Существует множество инструментов, которые могут снимать и просматривать тред-дампы. Несмотря на различия в формате и детализации, дампы, создаваемые разными инструментами, в целом выглядят схожим образом.

Вот пример того, как поток описывается в дампе:

"main" prio=5 tid=0x000001f3c9d13000 nid=NA runnable

  java.lang.Thread.State: RUNNABLE

    at java.net.SocketInputStream.socketRead0(Native Method)

    at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)

    at java.net.SocketInputStream.read(SocketInputStream.java:171)

    at java.net.SocketInputStream.read(SocketInputStream.java:141)

    at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)

      - locked <0x00000007ab1d3fa8> (a java.io.InputStreamReader)

    at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)

    at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)

Тред-дамп включает в себя стектрейсы потока, метаданные, статус и информацию о связанных блокировках.

Тред-дампы виртуальных потоков


Поскольку виртуальные потоки под капотом работают иначе, для их анализа требуется специализированная поддержка. Помимо IntelliJ IDEA или Open IDE, распространёнными инструментами для снятия дампов виртуальных потоков являются jcmd и jstack. Как уже упоминалось, обычные инструменты создания тред-дампов различаются по формату и уровню предоставляемой информации. Выбор того или иного инструмента зависит от конкретного сценария использования.

Пример Netflix


Чтобы перейти от теории к практике, рассмотрим случай из опыта компании Netflix. Если вы пропустили их публикацию на эту тему, настоятельно рекомендуем её прочитать. Это увлекательный технический разбор от инженеров, которые делают впечатляющие штуки на Java, и он напрямую относится к нашему обсуждению.

Кратко говоря, после перехода на виртуальные потоки инженеры Netflix столкнулись с проблемой: некоторые эндпоинты перестали обслуживать трафик, несмотря на то что JVM продолжала работать. Чтобы разобраться в происходящем, команда проанализировала блокировки с помощью дампа кучи/heap dump-а (!) и провела реверс-инжиниринг классов которые используются в Java для конкарренси. Расследование выявило взаимную блокировку, связанную с ограничением виртуальных потоков. Этот баг впоследствии был исправлен в Java 25.

Как бы ни были изобретательны их методы, трудно не согласиться с тем, что тред-дамп с более полной информацией значительно упростил бы расследование.

Получение информации о блокировках для виртуальных потоков


Если вы пользуетесь IntelliJ IDEA, вам повезло! Команда IntelliJ IDEA задаёт высокий приоритет новым релизам Java, поэтому как только в платформе появляется новая  фича, вы можете быть уверены, что поддержка в IDE не заставит себя ждать. Позже в этой статье мы подробнее рассмотрим возможности инструмента тред-дампов в IntelliJ IDEA, а пока сосредоточимся на самой проблеме.

Используя код из оригинальной статьи, можно установить брейкпоинт на строку, где ловится взаимная блокировка, и запустить воспроизводящий пример в режиме отладки:

Мы используем здесь брейкпоинт потому, что в противном случае пример из gist завершился бы слишком быстро, и мы не успели бы снять тред-дамп. В реальных приложениях вы можете получить тред-дамп в любой момент, не приостанавливая выполнение программы. Для этого нужно перейти в раздел More | Get thread dump на вкладке Debug целевого приложения.

IntelliJ IDEA откроет тред-дамп, отобразит статусы потоков, сгруппирует схожие потоки, свернёт стектрейсы и обеспечит навигацию обратно к исходному коду. При выборе ожидающего виртуального потока отображается та же информация, что и для платформенного потока, включая детали о захваченных блокировках:

В этом упрощённом примере виртуальные потоки синхронизируются на объекте new Object(), поэтому обнаружение конкретной блокировки не особенно полезно. Тем не менее, информация присутствует — и она бесценна при диагностике реальных проблем.

С чем это работает


IntelliJ IDEA может подключаться к любому Java- или Kotlin-процессу — как локальному, так и удалённому.
При работе в удалённой среде вы можете собирать информацию о процессах удалённо, а затем просматривать и экспортировать дампы локально.

При этом тред-дампы вовсе не обязаны быть сгенерированы в IntelliJ IDEA. Если вам предоставили дамп, созданный другим инструментом — например, jcmd — IntelliJ IDEA сможет открыть и проанализировать его так же легко, как и собственные дампы. Мы постоянно следим за планируемыми изменениями в экосистеме, так что вы можете рассчитывать на поддержку как старых, так и новых версий.

И бонус для пользователей Kotlin: IntelliJ IDEA поддерживает не только виртуальные потоки, но и корутины Kotlin!

Итоги


Java стремительно развивается и внедряет современные возможности в сферу многопоточных вычислений — и это только начало. Structured concurrency, настоящая смена парадигмы в подходе Java-разработчиков к многопоточности, уже не за горами. В этом процессе участвуем мы все — и вендоры, и пользователи.

Мы приглашаем вас опробовать работу с тред-дампами в IntelliJ IDEA — вы убедитесь, что эта статья лишь приоткрыла завесу над темой. Также рекомендуем заглянуть в нашу документацию — она всегда к вашим услугам. А для обратной связи и предложений по улучшению функциональности добро пожаловать в раздел комментариев или в наш трекер задач.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.