С выходом JDK 21 появилась широко разрекламированная новая функция — виртуальные потоки Java. Это нововведение помогает разработчикам на Java лучше управлять параллелизмом в своих приложениях. Основные цели виртуальных потоков Java включают:

  • лёгкую, масштабируемую и удобную для пользователя модель конкурентности

  • эффективное использование ресурсов системы

  • «значительное сокращение усилий на написание, сопровождение и наблюдение высокопроизводительных многопоточных приложений» (JEP425)

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

В этой статье представлены наши результаты, включая:

  • Обзор реализации виртуальных потоков в Java

  • Обзор текущей технологии пула потоков в Liberty

  • Оценку по ряду показателей производительности, включая некоторые неожиданные наблюдения

  • Сводный обзор наших выводов

Ключевые выводы статьи
  • Виртуальные потоки — важное новшество в многопоточном программировании на Java. Однако для выполнения типичных облачно-ориентированных Java-нагрузок они не обеспечивают явного преимущества по сравнению с адаптивным пулом потоков Open Liberty.

  • Для нагрузок, которые интенсивно используют CPU, пропускная способность ниже при использовании виртуальных потоков, чем при использовании пула потоков Open Liberty, однако причины этого пока остаются неясными.

  • Виртуальные потоки достигают полной пропускной способности быстрее при переходе из состояния простоя, чем пул потоков Open Liberty, благодаря модели «один поток на запрос».

  • Объём используемой памяти в развёртываниях Open Liberty может значительно варьироваться в зависимости от некоторых факторов (дизайн приложения, уровень нагрузки и поведение сборки мусора), поэтому уменьшенный объём памяти, потребляемый виртуальными потоками, не обязательно приводит к общему снижению потребления памяти.

  • Виртуальные потоки показали неожиданные проблемы с производительностью в отдельных случаях, о которых разработчикам на Java стоит знать. Мы работаем с сообществом OpenJDK, чтобы выяснить главную причину этих проблем и попытаться их устранить.

Оглавление:

  1. Виртуальные потоки Java

  2. Саморегулирующийся пул потоков Open Liberty

  3. Тесты производительности

    1. Среда тестирования

    2. Тестовый случай 1: Пропускная способность CPU

    3. Тестовый случай 2: Время разгона

    4. Тестовый случай 3: Объём используемой памяти

    5. Неожиданные результаты производительности виртуальных потоков

  4. Подведение итогов


Виртуальные потоки Java

Виртуальные потоки были впервые представлены в JDK 19, усовершенствованы в JDK 20 и окончательно включены в JDK 21 (описано в предложении по улучшению JDK (JEP) 444).

Традиционно Java-разработчики использовали модель «один поток на запрос», в которой для обработки каждого запроса создавался отдельный поток на протяжении всего жизненного цикла запроса. Эти потоки (известные как платформенные потоки) реализованы как обёртки вокруг потока операционной системы. Однако потоки ОС потребляют значительное количество системной памяти и управляются на уровне операционной системы, что может приводить к проблемам с масштабированием при увеличении их числа.

Одной из ключевых мотиваций для создания виртуальных потоков стало сохранение простоты модели «один поток на запрос», но с устранением высокой стоимости выделенных потоков ОС. Виртуальные потоки решают эту проблему, создавая каждый поток как легковесный объект в куче Java и назначая потоки ОС только по мере необходимости. Такое «разделение» потоков ОС позволяет более эффективно использовать системные ресурсы. В теории это значительное преимущество для виртуальных потоков: теперь разработчики могут эффективно использовать «миллионы потоков» в одном экземпляре JVM.

На следующей схеме показано соотношение «многие-к-одному» между виртуальными потоками Java и потоками ОС.

Саморегулирующийся пул потоков Open Liberty

Подход общего пула потоков в Open Liberty также позволяет снизить высокие издержки на выделенные потоки ОС. Liberty использует пул потоков (называемый «пулом потоков Liberty») для выполнения бизнес-логики приложения, а отдельные потоки — для функций ввода-вывода. Кроме того, пул потоков Liberty адаптивен и автоматически масштабируется, что позволяет ему подстраиваться под текущую нагрузку. В большинстве случаев дополнительная настройка не требуется, хотя минимальные и максимальные размеры пула можно заданы вручную.

В отличие от веб-сервера (например, Helidon Web Server, реализованного с использованием виртуальных потоков), среда выполнения приложений Liberty не просто устанавливает соединение ввода-вывода, которое затем находится в ожидании. Приложения, работающие на Liberty, обычно выполняют заметный объём бизнес-логики, что требует ресурсов процессора. Liberty не требует тысячи или миллионы потоков, так как ресурсы CPU полностью потребляются несколькими сотнями потоков (или даже меньшим количеством), особенно в контейнерах или подах, где выделено всего несколько процессоров или даже их доли.

Тесты производительности

Мы сосредоточили нашу оценку в основном на сценариях использования и конфигурациях, которые часто применяются клиентами Liberty. Для сравнения относительной производительности пула потоков Liberty и виртуальных потоков мы использовали наши существующие тестовые приложения. Эти приложения работают с REST и MicroProfile и выполняют базовую бизнес-логику при обработке транзакций.

Мы стремились смоделировать, что могли бы увидеть большинство пользователей Liberty, если бы заменили самонастраивающийся пул потоков Liberty на виртуальные потоки. Поэтому мы сосредоточились на конфигурациях с десятками или сотнями потоков. Тем не менее, мы также расширили тесты, чтобы сравнить поведение пула потоков Liberty и виртуальных потоков в условиях нескольких тысяч потоков, поскольку поскольку это считается одной из сильных сторон виртуальных потоков.

Чтобы оценить сценарий, в котором происходят действия размонтирования и монтирования виртуальных потоков, мы использовали приложение для симуляции онлайн-банкинга, которое генерирует запрос к удалённой системе и получает ответ с регулируемой задержкой. Задержка в ответе означает, что потоки в тестируемой системе блокируются на операции ввода-вывода и некоторое время не задействуют CPU. Это приложение создаёт тип нагрузки, при котором виртуальные потоки могут быть размонтированы в процессе транзакции и вновь смонтированы после получения ответа от удалённой системы (то есть позволяет потокам ОС распределяться между виртуальными потоками).

Среда тестирования

Мы проводили тесты производительности как с Eclipse Temurin (OpenJDK с JVM HotSpot), так и с IBM Semeru Runtimes (OpenJDK с JVM OpenJ9). В обоих случаях мы наблюдали сходные различия в производительности между пулом потоков Liberty и виртуальными потоками. Если не указано иначе, результаты, приведённые ниже, были получены с использованием Liberty 23.0.0.10 GA и релиза Temurin 21.0.1_12.

Уточнение: Наша оценка виртуальных потоков была сосредоточена на выяснении, принесёт ли использование виртуальных потоков вместо саморегулирующегося пула потоков Liberty преимущества по части производительности для пользователей Liberty, при условии реализации модели «один поток на запрос», как указано ранее. Этот контекст важен при ознакомлении с тестовыми случаями, так как результаты могут сильно отличаться для других сред выполнения приложений, где нет самонастраивающегося пула потоков, как в Liberty.

Тестовый случай 1: Пропускная способность CPU

Цель: Оценить пропускную способность CPU и определить, снижается ли производительность при использовании виртуальных потоков по сравнению с пулом потоков Liberty.

Результаты: Для некоторых конфигураций пропускная способность при использовании виртуальных потоков оказалась на 10-40% ниже, чем при использовании пула потоков Liberty. 

Для этого теста мы запустили несколько CPU-нагруженных приложений и сравнили, сколько транзакций в секунду (tps) может быть выполнено при заданном количестве CPU, с виртуальными потоками и пулом потоков Liberty. Мы использовали Apache JMeter для задания различных уровней нагрузки, постепенно увеличивая использование CPU на небольшом сервере.

В одном из примеров мы запустили приложение для онлайн-банкинга с короткой задержкой в 2 мс, чтобы в каждом задании задействовались функции виртуальных потоков (монтирование/размонтирование/перемонтирование на потоках ОС), в то время как приложение в целом оставалось довольно требовательным к ресурсам CPU. Нагрузка постепенно увеличивалась, и на каждом уровне нагрузка поддерживалась в течение 150 секунд для получения стабильного среднего значения пропускной способности.

При низких уровнях нагрузки пропускная способность виртуальных потоков для приложения онлайн-банкинга была примерно равна пропускной способности пула потоков Liberty (см. график), при этом виртуальные потоки использовали немного больше CPU (использование CPU не показано). По мере увеличения нагрузки количество транзакций в секунду для виртуальных потоков постепенно отставало от пула потоков Liberty.

Мы ожидали, что виртуальные потоки могут быть несколько медленнее в приложении, интенсивно использующем CPU, поскольку виртуальные потоки не ускоряют выполнение кода по сравнению с традиционными потоками платформы Java, а также добавляют некоторую нагрузку, включая:

  • Монтирование и размонтирование: Виртуальные потоки монтируются на платформенный поток для выполнения задач и размонтируются в точках блокировки и по завершении выполнения. Кроме того, на каждое действие монтирования и размонтирования JVM отправляет уведомления через интерфейс JVM Tool Interface (JVMTI). Эти действия легковесны, но не бесплатны.

  • Сборка мусора: Для каждой транзакции создаётся и затем удаляется объект виртуального потока, что увеличивает затраты на выделение памяти и сборку мусора.

  • Потеря потокозависимого контекста: В Liberty используются переменные ThreadLocal для передачи общей информации между запросами. Эффективность этого подхода теряется при работе с виртуальными потоками, поскольку ThreadLocal удаляется вместе с виртуальным потоком. В рамках проекта мы перевели основные случаи использования ThreadLocal на другие механизмы, не привязанные к потокам, однако некоторые менее значительные случаи остались.

Тем не менее, профилирование CPU показало, что ни один из этих накладных расходов виртуальных потоков не был достаточно велик, чтобы объяснить наблюдаемое несоответствие в пропускной способности. В разделе «Неожиданные результаты производительности виртуальных потоков» мы обсудим другие возможные причины.

Виртуальные потоки не обеспечили прироста производительности по сравнению с запуском того же кода на обычных платформенных потоках Java в пуле потоков Liberty для CPU-ориентированных приложений на небольшом количестве процессоров (типичный случай использования Liberty).

Тестовый случай 2: Время разгона

Цель: Оценить, как быстро виртуальные потоки достигают полной пропускной способности по сравнению с пулом потоков Liberty.

Результаты: При внезапной подаче высокой нагрузки приложения, работающие на виртуальных потоках, достигают максимальной пропускной способности значительно быстрее, чем при работе на пуле потоков Liberty.

Простая модель использования виртуальных потоков предполагает, что каждая задача получает свой собственный (виртуальный) поток для выполнения, поэтому наш прототип виртуальных потоков Liberty создавал новый виртуальный поток для выполнения каждой задачи, поступившей от драйвера нагрузки. Таким образом, при использовании виртуальных потоков каждая задача немедленно получает поток для выполнения, тогда как в пуле потоков Liberty задача может ожидать освобождения потока.

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

Обработка тысяч потоков в пуле потоков Liberty

Мы обнаружили, что пул потоков Liberty справляется с несколькими тысячами потоков без проблем. Учитывая трудности при использовании большого количества платформенных потоков, мы были готовы к потенциальным проблемам в пуле потоков Liberty. Например, он мог бы терять стабильность при обработке нескольких тысяч потоков или демонстрировать другие признаки проблемы «слишком много потоков». Однако никаких подобных проблем не возникло.

Наоборот, мы обнаружили, что пропускная способность пула потоков Liberty была даже выше на 2–3% по сравнению с виртуальными потоками. Загрузка CPU была примерно на 10% ниже в пуле потоков Liberty, а количество транзакций на единицу загрузки CPU было на 12–15% выше, в основном благодаря самонастраивающемуся управлению размером пула потоков Liberty. Самонастраивающаяся система пула потоков Liberty позволяет пулу расти до тысяч потоков при необходимости для выполнения нагрузки, при этом обеспечивая стабильную работу.

Время разгона пула потоков Liberty по сравнению с виртуальными потоками

При тестировании масштабирования время разгона виртуальных потоков от низкой нагрузки до полной мощности было очень быстрым. Разгон пула потоков Liberty шёл медленнее, так как он регулируется постепенно на основе наблюдаемой пропускной способности; решения о росте, уменьшении или сохранении размера пула принимаются с интервалом 1500 мс, и может потребоваться несколько десятков минут, чтобы постепенно увеличить число потоков для обработки нагрузки.

В результате этого тестирования мы изменили алгоритмы самонастраивающегося управления пулом потоков Liberty, чтобы более агрессивно увеличивать размер пула, когда доступно больше свободных ресурсов CPU и очередь запросов пула потоков Liberty глубока. С этим изменением (доступно в Open Liberty 23.0.0.10 и более новых версиях) при внезапном увеличении нагрузки (около 30 секунд) на приложение для онлайн-банкинга, работающего на пуле потоков Liberty, вместо десятков минут оно теперь достигает пиковой пропускной способности примерно за 20–30 секунд после того, как то же приложение достигает максимума на виртуальных потоках — даже при нагрузке, требующей около 6000 потоков на незагруженной JVM (см. график).

Прототип виртуальных потоков всё ещё разгонялся быстрее, поскольку каждому запросу по мере его поступления предоставлялся новый виртуальный поток; но разница в скорости разгона между виртуальными потоками и пулом потоков Liberty значительно сократилась.

Тестовый случай 3: Объём используемой памяти

Цель: Определить, сколько памяти использует процесс Java при постоянной нагрузке как для виртуальных потоков, так и для пула потоков Liberty.

Результаты: Низкие требования виртуальных потоков к памяти на поток оказали лишь относительно небольшое прямое влияние в конфигурации, требующей несколько сотен потоков, и могут быть компенсированы другими источниками использования памяти в JVM.

Виртуальные потоки используют меньше памяти (размер процесса Java), чем традиционные платформенные потоки, поскольку им не требуется выделенный поток ОС. В данном тестовом случае мы измеряли, как это преимущество по памяти на поток отразилось на общем использовании памяти JVM при типичных уровнях нагрузки Liberty. Результаты оказались неоднозначными.

Мы ожидали, что при нагрузке с виртуальными потоками всегда будет использоваться меньше памяти, чем при той же нагрузке с пулом потоков Liberty. Однако вместо этого мы обнаружили, что в некоторых случаях конфигурация с виртуальными потоками использовала меньше памяти, а в других — больше.

Эта изменчивость возникла из-за того, что на использование памяти процессом Java влияли и другие факторы, помимо реализации потоков. Одним из элементов, оказавших существенное влияние на изменчивость использования памяти в нашем тестировании, были DirectByteBuffers (DBB), которые являются частью сетевой инфраструктуры Java.

DirectByteBuffers

DirectByteBuffers являются двухкомпонентной структурой: небольшой ссылочный объект в куче Java и область памяти переменного размера (обычно значительно большего объёма) в нативной или off-heap памяти. Ссылочный объект Java освобождается после того, как становится ненужным, и затем собирается сборщиком мусора — после чего освобождается связанная нативная память. Если объект DirectByteBuffers живёт достаточно долго и перемещается в область old-gen (в типичной поколенческой модели сборки мусора Java), выделенная нативная память удерживается до глобальной сборки мусора. Поскольку глобальные сборки мусора происходят редко, это выделение и удержание может привести к тому, что размер процесса Java будет значительно больше, чем реальное использование в режиме выполнения.

Примечание: Тест выполнялся с небольшой минимальной и относительно большой максимальной кучей, чтобы показать изменчивость использования кучи как один из факторов, влияющих на общее использование памяти JVM.

В некоторых случаях, когда нагрузка с виртуальными потоками занимала больше памяти, чем при той же нагрузке с пулом потоков Liberty, мы обнаружили, что разница была связана с удержанием DirectByteBuffers в памяти. Это не свидетельствует о проблеме с виртуальными потоками: длительность удержания памяти DirectByteBuffers зависит от взаимодействия нескольких факторов, таких как продолжительность транзакции, размер зоны nursery кучи Java и время продвижения объектов в old-gen. Мы могли бы провести тот же тест с немного другой конфигурацией или настройкой — и виртуальные потоки использовали бы меньше памяти, чем пул потоков Liberty, причем разница также была бы связана с удержанием DirectByteBuffers.

Например, рост нагрузки на 10% привёл к снижению использования памяти на 25% для приложения онлайн-банкинга, работающего на пуле потоков Liberty, но к увеличению использования памяти на 185% для того же приложения на виртуальных потоках (см. графики).

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

В работе по оценке производительности широко известно выражение YMMV. Некоторые пользователи виртуальных потоков увидят снижение общего использования памяти в системе, а другие — увеличение. Лишь небольшая часть этих изменений будет непосредственно связана с виртуальными потоками.

Неожиданные результаты производительности виртуальных потоков

В рамках нашего исследования виртуальных потоков мы провели множество экспериментов с нашими тестовыми приложениями, изменяя количество процессоров, нагрузку, задержку удалённого ответа (для приложения онлайн-банкинга), размер кучи и другие параметры. Эти эксперименты выявили несколько неожиданных результатов, которые не охвачены в предыдущих разделах.

В частности, при выполнении задач с короткой длительностью на двух процессорах мы иногда наблюдали очень низкую производительность виртуальных потоков. Мы установили, что это связано с тем, как планировщик ядра Linux взаимодействует с управлением потоками ForkJoinPool в Java. Более новые версии планировщика ядра Linux изменили взаимодействие с ForkJoinPool, но проблема с производительностью сохранялась, хотя и проявлялась иначе. Пользователи виртуальных потоков могут столкнуться с аналогичными проблемами и должны быть в курсе, что обновление до более новых ядер Linux лишь меняет характер поведения, а не устраняет проблему.

В этом тесте мы использовали наше тестовое приложение MicroProfile, mp-ping, которое выполняет простой «пинг» REST-сервиса. Драйвер нагрузки отправляет запрос к REST-URL приложения mp-ping, работающего на Liberty, и получает немедленный пинг-ответ (0,05–0,10 мс).

Низкая пропускная способность и низкая загрузка CPU при работе на виртуальных потоках

Мы обнаружили, что выполнение коротких задач (mp-ping) на виртуальных потоках с конфигурацией на двух CPU приводило к значительно более низкой пропускной способности, чем при работе на пуле потоков Liberty, а также к соответственно меньшему использованию CPU. Пропускная способность на виртуальных потоках составила лишь 50–55% от показателя пула потоков Liberty, как видно на следующем графике.

Плохая производительность также наблюдалась при более длительных задачах, до 1 мс, и при использовании большего числа CPU, но с менее выраженным эффектом.

Мы воспроизвели проблему низкой пропускной способности и низкой загрузки CPU при использовании виртуальных потоков на нескольких различных аппаратных платформах с разными версиями ядра Linux, чтобы убедиться, что это поведение не является результатом какой-либо особенности исходной тестовой системы. Мы также создали простое автономное приложение, которое генерирует задачи с нагрузкой на CPU в течение настраиваемого периода, и оно показало аналогичную низкую пропускную способность и низкую загрузку CPU с виртуальными потоками, что подтверждает, что проблема не вызвана самой Liberty.

ForkJoinPool и планировщик ядра Linux

Исследование главной причины низкой производительности виртуальных потоков показало, что Java ForkJoinPool, который управляет платформенными потоками, поддерживающими виртуальные потоки, останавливал один из потоков на 10-13 мс даже в условиях достаточной загрузки. С одним припаркованным платформенным потоком виртуальные потоки не могли своевременно начать выполнение, что и приводило к наблюдаемой низкой пропускной способности и низкой загрузке CPU.

Дальнейшее исследование указало на планировщик потоков Linux: трассировка показала вызовы для «разпарковки» припаркованного платформенного потока в коде ForkJoinPool, но эти вызовы не приводили к его немедленному возвращению в работу. Мы пришли к выводу, что низкая производительность вызвана взаимодействием планировщика потоков Linux и управления рабочими потоками ForkJoinPool. Эта проблема не возникала в пуле потоков Liberty, так как он не использует ForkJoinPool для управления платформенными потоками.

Мы протестировали доступные параметры настройки ForkJoinPool, параметры настройки планировщика Linux и различные изменения в реализации ForkJoinPool, что дало некоторые незначительные улучшения производительности, но не позволило значительно приблизить её к производительности пула потоков Liberty.

Примечание: Наше исследование показало, что работа с двумя CPU, вероятно, является наихудшим сценарием для выявленных нами проблем с виртуальными потоками в ядре Linux 4.18. Проблемы с производительностью сохранялись, но были менее выражены при выполнении тех же нагрузок на тестовых системах с одним или четырьмя и более CPU.

Низкая пропускная способность и высокая загрузка CPU при работе на виртуальных потоках

Тестирование, описанное в предыдущих разделах, проводилось в основном на ядре Linux 4.18, которое является текущей версией в Red Hat Enterprise Linux (RHEL) 8. Мы обнаружили другую проблему с производительностью виртуальных потоков, когда провели те же тесты на более новых ядрах Linux 5.14 (RHEL 9) и ядре 6.2 (Ubuntu 22.04).

С более новыми ядрами Linux приложение mp-ping на виртуальных потоках по-прежнему показывало несколько более низкую пропускную способность по сравнению с пулом потоков Liberty, но с более высокой загрузкой CPU. По мере увеличения нагрузки пропускная способность виртуальных потоков была на 20–30% ниже, чем у пула потоков Liberty, что видно на следующем графике.

Эти результаты показывают, что с виртуальными потоками могут возникать разные вопросы производительности для некоторых типов нагрузок, в зависимости от версии ядра Linux.

Следующие шаги по исследованию причины этих проблем

Мы обсудили полученные результаты с участниками сообщества OpenJDK и продолжаем исследовать и тестировать модификации совместно с ними. Тесты, представленные на графиках, использовали последнюю ночную сборку Temurin 22 для работы с новейшей версией ForkJoinPool, который сейчас пересматривается, в надежде, что обновления ForkJoinPool устранят изначально наблюдаемые нами проблемы с Temurin 21 (но этого не произошло).

Необходимо дальнейшее исследование для полного определения причины и возможного решения, и мы активно работаем над этим с сообществом OpenJDK. Мы хотим выразить признательность Дагу Ли (ведущему специалисту в области Java-конкурентности и автору класса ForkJoinPool) и другим участникам сообщества OpenJDK за помощь в нашем исследовании проблем производительности виртуальных потоков. Мы сообщаем об этих проблемах для предупреждения пользователей виртуальных потоков, которые могут столкнуться с похожими ситуациями в зависимости от сценария их использования.

Для тех, кто заинтересован в воспроизведении проблем, описанных в разделе «Неожиданные результаты», мы предоставили README с инструкциями в репозитории на GitHub.

Подведение итогов

Мы изучили производительность виртуальных потоков с использованием простых приложений, представляющих типичные случаи использования Liberty, и сосредоточились на трёх основных аспектах производительности:

  • Пропускная способность: Виртуальные потоки показали худшую производительность по сравнению с пулом потоков Liberty в протестированных приложениях. Снижение производительности наблюдалось на разных уровнях в зависимости от количества CPU, длительности задач, версии ядра Linux и настроек планировщика Linux.

  • Время разгона: При поступлении нагрузки всплесками, с задачами длительного выполнения, требующими множества потоков, виртуальные потоки быстрее достигают полной пропускной способности по сравнению с пулом потоков Liberty, но это преимущество быстро исчезает.

  • Объём памяти: Эффект меньшего объёма памяти на поток у виртуальных потоков оказался относительно небольшим при конфигурации с несколькими сотнями потоков и может быть нивелирован другими источниками использования памяти в JVM.

Кроме того, нас удивило обнаружение проблемы с производительностью в некоторых случаях использования виртуальных потоков. Мы установили, что она вызвана взаимодействием планировщика ядра Linux и управления потоками ForkJoinPool в Java. Эта проблема сохраняется, хотя и в другой форме, даже с новыми версиями ядра.

Сравнив существующее управление потоками Liberty с новыми виртуальными потоками Java, мы обнаружили, что текущий пул потоков Liberty обеспечивает сопоставимую или даже лучшую производительность для Liberty (а значит, и для любых приложений, работающих на Liberty) при умеренно высоком уровне конкурентности (в тысячи потоков). Виртуальные потоки могут демонстрировать преимущества при более высоких уровнях конкурентности по сравнению с пулом потоков Liberty, но это зависит от определённых условий, таких как длительные задержки в задачах, большое количество CPU или комбинация этих факторов.

Разработчики Java-приложений по-прежнему могут использовать виртуальные потоки в своих приложениях, работающих на Liberty, но мы решили пока не заменять пул потоков Liberty на виртуальные потоки. Как говорилось ранее в статье, существуют сценарии, где виртуальные потоки полезны для упрощения разработки многопоточных приложений. Однако, как показано, разработчикам следует знать о возможных проблемах в определённых типах приложений. Надеемся, что, поделившись нашим опытом, мы поможем разработчикам Java лучше понять, когда и стоит ли использовать виртуальные потоки в их приложениях.


В заключение рекомендуем обратить внимание на следующие открытые уроки:

  • 6 ноября: Docker-compose и его использование для разворачивания инфраструктуры тестирования. Подробнее

  • 20 ноября: Многопоточность и futures в Java и их применение в автоматизации тестирования. Подробнее

Комментарии (0)