Многопоточность в Java с Project Loom: виртуальные или обычные потоки
Многопоточность всегда была одноим из основных элементов в разработке современного программного обеспечения. Она позволяет приложениям обрабатывать несколько задач одновременно, что критично для создания отзывчивых и масштабируемых систем. Project Loom — амбициозный проект Oracle, который обещает революцию в многопоточности в Java. Вместо того чтобы предлагать очередную библиотеку или фреймворк, Loom вносит изменения в саму виртуальную машину Java, вводя концепцию виртуальных потоков. В чем же ключевая разница? Традиционные потоки Java (теперь их называют "платформенными") имеют прямую связь с потоками операционной системы, что делает их тяжеловесными и ограниченными в количестве. Виртуальные потоки, напротив, управляются JVM и не имеют такой строгой привязки. Это позволяет создавать их в огромном количестве без сущeственных накладных расходов. Если раньше система с несколькими тысячами одновременных соединений требовала сложной асинхронной архитектуры, то с виртуальными потоками она может быть реализована гораздо проще и при этом потреблять меньше ресурсов. Но, как и любая новая технология, Project Loom не является панацеей. У него есть свои ограничения и особенности применения. Например, он не решает проблемы с CPU-интенсивными задачами так же эффективно, как с IO-интенсивными. Технические основыЧтобы понять Project Loom, нужно сначала разобраться с тем, как функционирует традиционная модель многопоточности в Java. С момента появления языка, многопоточность в Java была основана на относительно прямолинейной концепции: один поток = один поток операционной системы. Классическая модель потоков в JavaПлатформенные потоки в Java — это прямая обёртка над потоками операционной системы. Когда мы создаём экземпляр Thread и вызываем метод start() , Java запрашивает у ОС выделение отдельного потока. Это даёт программам доступ к настоящему параллелизму на многоядерных системах, но также приносит определённые ограничения.
Соотношение между ОС-потоками и потоками JavaВ классической модели соотношение между потоками ОС и потоками Java составляет 1:1. Каждый поток Java требует отдельного потока ОС. Это приводит к известным проблемам масштабирования, когда дело доходит до тысяч одновременных операций, особенно если многие из них проводят большую часть времени в ожидании (например, сетевых ответов или операций с диском). Для решения этой проблемы разработчики обычно использовали пулы потоков:
Виртуальные потоки Project LoomProject Loom радикально меняет подход к потокам, вводя концепцию виртуальных потоков. Виртуальные потоки управляются JVM, а не напрямую операционной системой. Это похоже на то, как JVM управляет памятью через сборщик мусора, а не заставляет программиста самостоятельно выделять и освобождать её.
1. Низкие накладные расходы - виртуальный поток занимает всего несколько килобайт памяти, а не мегабайт. 2. Масштабируемость - можно создавать миллионы виртуальных потоков на обычном оборудовании. 3. Упрощённая модель программирования - можно писать понятный, последовательный код, не прибегая к сложным асинхронным конструкциям. Пример использования виртуальных потоков для обработки множества запросов:
Модель управления планировщиком в виртуальных потокахОдной из ключевых инноваций Project Loom является план-монополизация (mount-unmount scheduling). Когда виртуальный поток блокируется на операции ввода-вывода, JVM автоматически отсоединяет его от потока-носителя, позволяя последнему выполнять другие виртуальные потоки. Когда операция ввода-вывода завершается, виртуальный поток снова монтируется на доступный поток-носитель. Это устраняет одну из главных проблем традиционных потоков – ситуацию, когда большое количество потоков ОС бездействует в заблокированном состоянии, потребляя при этом системные ресурсы. В виртуальных потоках операции IO по-прежнему являются блокирующими с точки зрения кода, но внутренняя реализация JVM делает их неблокирующими с точки зрения потоков-носителей. Это достигается через инструментарий методов Java API, которые являются блокирующими. Когда вызывается такой метод, JVM перехватывает вызов и освобождает поток-носитель, позволяя ему выполнять другую работу, пока блокирующая операция не завершится.
Структурированный параллелизм с использованием виртуальных потоковProject Loom также вносит существенный вклад в концепцию структурированного параллелизма через виртуальные потоки. Традиционная модель потоков в Java часто приводит к созданию "бесхозных" потоков, которые сложно контролировать и отслеживать. Структурированный параллелизм предлагает более дисциплинированный подход. Одним из нововведений является StructuredTaskScope — класс, который позволяет создавать иерархическую структуру задач и обеспечивать их правильное завершение:
Сравнение архитектурПри сравнении традиционных и виртуальных потоков важно понимать архитектурные отличия: 1. Управление ресурсами: - Традиционные потоки: Прямое отображение на потоки ОС, высокие затраты памяти (около 1 МБ на поток). - Виртуальные потоки: Управляются JVM, минимальные затраты памяти (всего несколько КБ на поток). 2. Масштабируемость: - Традиционные потоки: Ограничены ресурсами ОС, редко больше нескольких тысяч на сервер. - Виртуальные потоки: Потенциально миллионы на одном сервере, ограничены лишь доступной памятью. 3. Модель программирования: - Традиционные потоки: Требуют тщательного управления пулами потоков, часто приводят к использованию асинхронных паттернов. - Виртуальные потоки: Позволяют использовать привычный синхронный код даже для высоко-параллельных задач. 4. Производительность: - Традиционные потоки: Высокие затраты на переключение контекста, особенно при большом количестве потоков. - Виртуальные потоки: Низкие затраты на переключение, эффективное использование потоков-носителей. Важно отметить, что виртуальные потоки не являются полной заменой традиционным потокам во всех сценариях. Для CPU-интенсивных задач, где вычисления производятся непрерывно без значительных периодов ожидания, преимущества виртуальных потоков менее очевидны. В таких случаях количество одновременно выполняющихся потоков всё равно будет ограничено количеством доступных процессорных ядер. Рассмотрим пример, показывающий разницу подходов при обработке множества HTTP-запросов:
Создать текстовый файл, записать туда информацию. прочесть, серилизовать и записать байтовый поток в другой файл Создание ссылки на поток Не создается поток Необходимо реализовать консольную программу, которая бы фильтровала поток текстовой информации, подаваемой на вход, и на выходе показывала лишь те стр Производительность и эффективностьНасколько виртуальные потоки эффективнее традиционных? В каких сценариях они показывают наибольшую разницу? Давайте разберёмся с практическими аспектами производительности. Оптимизация времени отклика в IO-интенсивных приложенияхВ IO-интенсивных приложениях — таких, как веб-серверы, микросервисы или системы работы с базами данных — виртуальные потоки демонстрируют впечатляющие результаты. Возьмём типичный веб-сервер, обрабатывающий тысячи HTTP-запросов, каждый из которых должен обращаться к базе данных и, возможно, к другим микросервисам. В традиционной модели каждый такой запрос либо занимает физический поток (что ограничивает количество одновременных запросов), либо требует сложной асинхронной архитектуры. У виртуальных потоков такого ограничения практически нет. Я провёл небольшой эксперимент с имитацией веб-сервера. Он выполнял 10 000 псевдо-запросов, каждый из которых включал:
Вот результаты замеров:
Разница в ~143 раза! И это без учёта дополнительной нагрузки на память и CPU, создаваемой большим количеством физических потоков. Если посмотреть на нагрузку процессора во время выполнения, то при использовании виртуальных потоков она была гораздо более равномерной, без характерных для традиционных потоков всплесков активности при переключении контекста. Сравнительный анализ утечек памяти в разных моделях потоковОдно из главных преимуществ виртуальных потоков — радикально меньшее потребление памяти. Проблемы с утечками памяти при работе с многопоточностью хорошо известны: 1. Забытые потоки, которые не были корректно остановлены.В случае с традиционными потоками каждая такая утечка стоит дорого. Один "потерянный" поток — это около 1 МБ памяти, плюс связанные с ним ресурсы ОС. Если таких потоков тысячи, последствия могут быть катастрофическими. Виртуальные потоки значительно снижают эту проблему. Во-первых, каждый виртуальный поток потребляет лишь несколько килобайт памяти. Во-вторых, благодаря интеграции с JVM, виртуальные потоки лучше подходят для сборки мусора — нет потоков ОС, постоянно удерживающих ресурсы. Приведу пример. Я создал тест, который намеренно провоцировал утечку потоков — создавал их, но не завершал корректно:
Тестовые сценарии и метрикиПри оценке производительности многопоточных систем важно рассматривать несколько ключевых метрик: 1. Пропускная способность — количество задач, которые система может обработать в единицу времени.Для объективной оценки я проводил тесты в нескольких сценариях: Сценарий "множество коротких задач": имитация микросервиса, обрабатывающего тысячи небольших запросов.Результаты были предсказуемы, но всё же впечатляют. В IO-интенсивных сценариях виртуальные потоки показали превосходство в 10-150 раз по пропускной способности по сравнению с традиционными потоками. В сценариях с короткими задачами разница была меньше, но всё равно существенна — 3-5 раз. Однако в CPU-интенсивных сценариях преимущество практически исчезало. Это логично: если потоки проводят большую часть времени в активном вычислении, а не в ожидании, то основным ограничением становится количество ядер процессора, а не эффективность управления парковкой/распарковкой потоков. Результаты бенчмарковДля более детального понимания приведу результаты бенчмарка веб-сервера на Spring Boot, который использовался для обработки синтетической нагрузки:
Память и ресурсыЕщё одно важное преимущество виртуальных потоков — сниженное давление на сборщик мусора. Традиционные потоки с их крупными стеками создают фрагментацию памяти и замедляют сборку мусора. Виртуальные потоки с их компактными структурами данных гораздо дружелюбнее к GC. Стоит отметить и время создания потоков. Создание традиционного потока — операция относительно дорогая, требующая системных вызовов. Создание виртуального потока выполняется значительно быстрее. В моих тестах создание 100 000 виртуальных потоков занимало около 500 мс, тогда как система даже не позволяла создать такое количество традиционных потоков. Эти преимущества делают виртуальные потоки идеальным выбором для систем, где требуется обработка множества параллельных, но не особо вычислительно интенсивных задач. Это пододит для большинства современных веб-приложений и микросервисов, которые проводят основную часть времени в ожидании ответов от баз данных или других сервисов. ПрименениеТеория виртуальных потоков выглядит многообещающей, но как насчёт применения Project Loom в реальных проектах? Какие проблемы могут возникнуть при миграции существующих систем и как их избежать? Сценарии миграцииПереход с традиционных потоков на виртуальные далеко не всегда требует полной переработки кода. Преимущество Project Loom заключается в том, что API виртуальных потоков совместим с существующим Thread API, что делает миграцию относительно безболезненной. Для базового случая миграция может быть настолько простой, как замена создания исполнителя:
1. Идентифицировать наиболее IO-интенсивные части приложения.Особое внимание следует уделить участкам кода, которые могут блокировать потоки на длительное время. Например, при работе с блокирующими очередями или длительными операциями ввода-вывода — именно здесь преимущества виртуальных потоков проявятся максимально. Интересный пример — миграция клиента базы данных. Большинство JDBC-драйверов используют блокирующие операции, что делает их идеальными кандидатами для виртуальных потоков:
Кастомные планировщики для виртуальных потоковОдной из малоизвестных возможностей Project Loom является предоставление API для создания собственных планировщиков для виртуальных потоков. Это позволяет тонко настроить поведение под конкретные нужды приложения. Например, можно создать планировщик, который будет выделять разные приоритеты разным типам задач:
Отладка и профилирование виртуальных потоковОтладка многопоточных приложений всегда была непростой задачей, а с введением виртуальных потоков появляются дополнительные нюансы. Например, при выполнении миллионов виртуальных потоков традиционные инструменты мониторинга могут оказаться неэффективными. JDK включает обновлённую версию инструмента jstack, который теперь умеет работать с виртуальными потоками. Тем не менее, из-за потенциально огромного количества виртуальных потоков необходимо использовать фильтрацию:
На практике я столкнулся с интересным случаем: приложение, перенесённое на виртуальные потоки, показывало странные всплески потребления CPU. Анализ через JFR выявил, что причина была не в самой технологии виртуальных потоков, а в том, что с их помощью мы стали делать больше параллельной работы, чем раньше, и нагрузка сместилась на другой узел системы. Примеры кода и реализацииРассмотрим несколько практических примеров, демонстрирующих возможности виртуальных потоков в типичных задачах. Пример 1: Веб-скраппер Задача сборки данных с множества веб-страниц отлично подходит для виртуальных потоков:
Для приложений, работающих с сообщениями, виртуальные потоки могут значительно повысить пропускную способность:
Типичные ошибкиПри переходе на виртуальные потоки разработчики часто сталкиваются с несколькими распространёнными проблемами: 1. Гонка за ресурсами: Возможность создавать огромное количество потоков может привести к исчерпанию других ресурсов, например, открытых файловых дескрипторов или соединений с базой данных. В таких случаях пул всё ещё может быть необходим, но уже на уровне специфических ресурсов, а не потоков.Я видел проект, где команда решила заменить все ExecutorService с фиксированным пулом на виртуальные потоки. Для большинства случаев это работало отлично, но одна конкретная задача — обработка больших медиафайлов — стала выполняться хуже. Оказалось, эта задача была CPU-интенсивной, и использование слишком большого количества параллельных потоков только создавало ненужные переключения контекста. Решением стал гибридный подход: виртуальные потоки для IO-задач и ограниченный пул обычных потоков для CPU-интенсивных операций:
Перспективы развитияРассмотрим, как этот проект может развиваться и взаимодействовать с другими технологиями. Взаимодействие Project Loom с реактивными фреймворкамиПоявление виртуальных потоков естественным образом поднимает вопрос об их сосуществовании с популярными реактивными фреймворками типа Spring Reactor, RxJava или Akka. На первый взгляд, эти технологии кажутся конкурирующими – обе решают проблему масштабируемости, но совершенно разными путями. Реактивное программирование получило широкое распространение как раз потому, что традиционные потоки не справлялись с высокой конкурентностью. Оно представляет асинхронную модель программирования с отложенными вычислениями и обработкой событий. В противовес этому, Project Loom предлагает вернуться к привычной синхронной модели, делая её масштабируемой. Однако вместо полного вытеснения, мы скорее увидим симбиоз этих подходов. Создатель Project Reactor Стефан Малар уже высказывался, что виртуальные потоки и реактивное программирование могут дополнять друг друга:
Экосистема и совместимостьДля широкого внедрения любой новой технологии принципиально важна поддержка со стороны сообществ и фреймворков. Project Loom получил значительную поддержку от ключевых игроков экосистемы Java. Spring Framework уже добавил встроенную поддержку виртуальных потоков в версии 6.0. Это позволяет использовать их в сервлетах и RESTful веб-приложениях с минимальными изменениями кода:
Хотя большинство библиотек Java совместимы с виртуальными потоками "из коробки", некоторые низкоуровневые компоненты, использующие thread-local переменные или предположения о соотношении "один поток = одна задача", могут требовать обновления. Рекомендации по выбору подходаНесмотря на все преимущества виртуальных потоков, они не являются универсальным решением для всех сценариев. Вот некоторые рекомендации по выбору между традиционными и виртуальными потоками: Виртуальные потоки рекомендуются, когда:
Традиционные потоки могут быть предпочтительнее, когда:
Идеальным решением часто оказывается гибридный подход, где разные типы задач обрабатываются разными моделями параллелизма. Project Loom не решает все проблемы конкурентного программирования. Он не избавляет от необходимости правильно обрабатывать общие ресурсы, синхронизировать доступ и предотвращать дедлоки. Эти фундаментальные аспекты многопоточного программирования остаются актуальными независимо от типа используемых потоков. В перспективе мы можем ожидать, что виртуальные потоки станут основным подходом для построения высокопроизводительных серверных приложений на Java, постепенно вытесняя сложные асинхронные фреймворки в пользу более простой и понятной модели программирования. Это может сделать язык Java более конкурентоспособным по сравнению с Golang, Rust и другими языками, изначально спроектированными для эффективной работы с большим количеством одновременных соединений. Программа фильтрующая поток текста Передать параметр в поток RMI. После выполнения программы поток не завершается. В NN4.7 не создается поток вывода для CGI-скрипта, хотя в IE все нормально Как разбудить поток после команды Thread.sleep(t) ? Массив Image в поток и обратно как организовать поток из сервлета Как скопировать папку используя байтовый поток? Как запустить некий поток не при обращении к веб-приложению? Один поток загружает CPU на 100% Не читается поток JSF и отдельный поток |