JEP 444: Virtual Threads
Virtual Threads (виртуальные потоки)
| Автор | Ron Pressler & Alan Bateman |
| Ответственный | Alan Bateman |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 21 |
| Компонент | core-libs |
| Обсуждение | loom dash dev at openjdk dot org |
| Связан с | JEP 436: Virtual Threads (Second Preview) |
| JEP 491: Synchronize Virtual Threads without Pinning | |
| Рецензенты | Alex Buckley |
| Одобрен | Brian Goetz |
| Создан | 2023/03/06 18:00 |
| Обновлён | 2025/10/30 21:01 |
| Задача | 8303683 |
Аннотация
Добавить в платформу Java Virtual Threads. Virtual Threads — это легковесные потоки, которые резко сокращают усилия на написание и сопровождение конкурентных приложений с высокой пропускной способностью, а также на наблюдение за ними.
История
Virtual Threads были предложены в статусе Preview (предварительная версия) в JEP 425 и вошли в JDK 19. Чтобы оставить время для отзывов и накопить больше опыта, они были снова предложены в статусе Preview в JEP 436 и вошли в JDK 20. Этот JEP предлагает сделать Virtual Threads окончательной возможностью в JDK 21 со следующими изменениями по сравнению с JDK 20, внесёнными с учётом отзывов разработчиков:
-
Virtual Threads теперь всегда поддерживают локальные переменные потока. Больше нельзя, как это было возможно в версиях Preview, создавать потоки Virtual Threads, у которых не может быть локальных переменных потока. Гарантированная поддержка локальных переменных потока позволяет использовать с Virtual Threads без изменений гораздо больше существующих библиотек и помогает переводить код, ориентированный на задачи, на Virtual Threads.
-
Потоки Virtual Threads, созданные напрямую с помощью API
Thread.Builder(в отличие от созданных черезExecutors.newVirtualThreadPerTaskExecutor()), теперь тоже по умолчанию отслеживаются в течение всего времени жизни, и их можно наблюдать в новом дампе потоков, описанном в разделе Наблюдение за Virtual Threads.
Цели
-
Позволить серверным приложениям, написанным в простом стиле «поток на запрос», масштабироваться с почти оптимальным использованием оборудования.
-
Позволить существующему коду, использующему API
java.lang.Thread, перейти на Virtual Threads с минимальными изменениями. -
Обеспечить простую диагностику, отладку и профилирование Virtual Threads с помощью существующих инструментов JDK.
Что не является целью
-
Цель не в том, чтобы удалить традиционную реализацию потоков или незаметно перевести существующие приложения на Virtual Threads.
-
Цель не в том, чтобы изменить базовую модель конкурентности Java.
-
Цель не в том, чтобы предложить новую конструкцию для параллелизма по данным ни в языке Java, ни в библиотеках Java. Stream API остаётся предпочтительным способом параллельной обработки больших наборов данных.
Мотивация
Уже почти три десятилетия Java-разработчики используют потоки как строительный блок конкурентных серверных приложений. Каждая инструкция в каждом методе выполняется внутри потока, и, поскольку Java — многопоточный язык, одновременно выполняется несколько потоков. Поток — это единица конкурентности Java: фрагмент последовательного кода, который выполняется конкурентно с другими такими единицами — и в значительной степени независимо от них. Каждый поток предоставляет стек для хранения локальных переменных и координации вызовов методов, а также контекст на случай, когда что-то идёт не так: исключения выбрасываются и перехватываются методами в одном и том же потоке, поэтому разработчики могут по трассировке стека потока выяснить, что произошло. Потоки — также центральное понятие для инструментов: отладчики пошагово проходят по инструкциям в методах потока, а профилировщики визуализируют поведение нескольких потоков, помогая понять их производительность.
Стиль «поток на запрос»
Серверные приложения обычно обрабатывают конкурентные запросы пользователей, независимые друг от друга, поэтому приложению имеет смысл обрабатывать запрос, выделяя ему поток на всё время его обработки. Этот стиль «поток на запрос» легко понять, на нём легко программировать, его легко отлаживать и профилировать, потому что он использует единицу конкурентности платформы для представления единицы конкурентности приложения.
Масштабируемость серверных приложений определяется законом Литтла, который связывает задержку, конкурентность и пропускную способность: при заданной длительности обработки запроса (т. е. задержке) число запросов, которые приложение обрабатывает одновременно (т. е. конкурентность), должно расти пропорционально частоте их поступления (т. е. пропускной способности). Например, допустим, приложение со средней задержкой 50 мс достигает пропускной способности 200 запросов в секунду, обрабатывая 10 запросов конкурентно. Чтобы это приложение масштабировалось до пропускной способности 2000 запросов в секунду, ему нужно будет обрабатывать 100 запросов конкурентно. Если каждый запрос обрабатывается в потоке на протяжении всей его обработки, то, чтобы приложение справлялось, число потоков должно расти вместе с ростом пропускной способности.
К сожалению, число доступных потоков ограничено, потому что JDK реализует потоки как обёртки над потоками операционной системы (ОС). Потоки ОС дороги, поэтому их не может быть слишком много, и из-за этого такая реализация плохо подходит для стиля «поток на запрос». Если каждый запрос занимает поток, а значит, и поток ОС, на всё время обработки, то число потоков часто становится ограничивающим фактором задолго до того, как будут исчерпаны другие ресурсы, например процессор или сетевые соединения. Текущая реализация потоков в JDK ограничивает пропускную способность приложения уровнем значительно ниже того, который может обеспечить оборудование. Так происходит даже при использовании пулов потоков, поскольку пул помогает избежать высоких затрат на запуск нового потока, но не увеличивает общее число потоков.
Повышение масштабируемости с помощью асинхронного стиля
Некоторые разработчики, желая использовать оборудование по максимуму, отказались от стиля «поток на запрос» в пользу стиля с разделением потоков. Вместо того чтобы обрабатывать запрос в одном потоке от начала до конца, код обработки запроса возвращает свой поток в пул, когда ждёт завершения очередной операции ввода-вывода, чтобы этот поток мог обслуживать другие запросы. Такое мелкозернистое разделение потоков — при котором код удерживает поток только во время вычислений, а не во время ожидания ввода-вывода — позволяет выполнять большое число конкурентных операций, не расходуя большое число потоков. Хотя это снимает ограничение на пропускную способность, вызванное нехваткой потоков ОС, цена высока: требуется так называемый асинхронный стиль программирования с отдельным набором методов ввода-вывода, которые не ждут завершения операций ввода-вывода, а позже сообщают о завершении через обратный вызов. Без выделенного потока разработчикам приходится разбивать логику обработки запроса на небольшие этапы, обычно записанные в виде лямбда-выражений, а затем собирать их в последовательный конвейер с помощью API (см., например, CompletableFuture или так называемые «реактивные» фреймворки). Тем самым они отказываются от базовых операторов последовательной композиции языка, таких как циклы и блоки try/catch.
В асинхронном стиле каждый этап запроса может выполняться в другом потоке, а каждый поток выполняет вперемешку этапы, относящиеся к разным запросам. Это глубоко влияет на понимание поведения программы: трассировки стека не дают полезного контекста, отладчики не могут пошагово пройти логику обработки запроса, а профилировщики не могут связать стоимость операции с её вызывающим кодом. Композиция лямбда-выражений вполне управляема, когда Stream API Java используется для обработки данных в коротком конвейере, но становится проблемой, когда так должен быть написан весь код обработки запросов в приложении. Этот стиль программирования расходится с платформой Java, потому что единица конкурентности приложения — асинхронный конвейер — больше не является единицей конкурентности платформы.
Сохранение стиля «поток на запрос» с помощью Virtual Threads
Чтобы приложения могли масштабироваться, оставаясь в согласии с платформой, нам следует стремиться сохранить стиль «поток на запрос». Этого можно добиться, реализовав потоки эффективнее, чтобы их могло быть больше. Операционные системы не могут реализовать потоки ОС эффективнее, потому что разные языки и среды выполнения используют стек потока по-разному. Однако среда выполнения Java может реализовать потоки Java так, чтобы разорвать их взаимно-однозначное соответствие потокам ОС. Подобно тому как операционные системы создают иллюзию обилия памяти, отображая большое виртуальное адресное пространство на ограниченный объём физической RAM, среда выполнения Java может создать иллюзию обилия потоков, отображая большое число потоков Virtual Threads на небольшое число потоков ОС.
Поток Virtual Threads — это экземпляр java.lang.Thread, не привязанный к конкретному потоку ОС. Платформенный поток, напротив, — это экземпляр java.lang.Thread, реализованный традиционным способом, как тонкая обёртка над потоком ОС.
Код приложения в стиле «поток на запрос» может выполняться в потоке Virtual Threads на протяжении всей обработки запроса, но этот поток занимает поток ОС только во время вычислений на процессоре. В результате получается та же масштабируемость, что и в асинхронном стиле, но достигается она прозрачно: когда код, выполняющийся в потоке Virtual Threads, вызывает блокирующую операцию ввода-вывода из API java.*, среда выполнения выполняет неблокирующий вызов ОС и автоматически приостанавливает поток Virtual Threads, пока его нельзя будет возобновить. Для Java-разработчиков Virtual Threads — это просто потоки, которые дёшево создавать и которых может быть почти бесконечно много. Использование оборудования близко к оптимальному, что обеспечивает высокий уровень конкурентности и, как следствие, высокую пропускную способность, при этом приложение остаётся в согласии с многопоточной архитектурой платформы Java и её инструментарием.
Следствия использования Virtual Threads
Virtual Threads дёшевы и многочисленны, поэтому их никогда не следует объединять в пулы: для каждой задачи приложения нужно создавать новый поток Virtual Threads. Поэтому большинство потоков Virtual Threads будут короткоживущими и с неглубокими стеками вызовов, выполняя всего лишь один вызов HTTP-клиента или один запрос JDBC. Платформенные потоки, напротив, тяжеловесны и дороги, поэтому их часто приходится объединять в пулы. Как правило, они долгоживущие, имеют глубокие стеки вызовов и используются совместно многими задачами.
Итак, Virtual Threads сохраняют надёжный стиль «поток на запрос», согласующийся с архитектурой платформы Java, и при этом оптимально используют доступное оборудование. Использование Virtual Threads не требует изучения новых концепций, хотя может потребовать отказа от привычек, выработанных, чтобы справляться с нынешней высокой стоимостью потоков. Virtual Threads помогут не только разработчикам приложений — они также помогут разработчикам фреймворков предоставлять простые в использовании API, совместимые с архитектурой платформы, без ущерба для масштабируемости.
Описание
Сегодня каждый экземпляр java.lang.Thread в JDK — это платформенный поток. Платформенный поток выполняет код Java в нижележащем потоке ОС и занимает этот поток ОС на всё время жизни кода. Число платформенных потоков ограничено числом потоков ОС.
Поток Virtual Threads — это экземпляр java.lang.Thread, который выполняет код Java в нижележащем потоке ОС, но не занимает этот поток ОС на всё время жизни кода. Это значит, что много потоков Virtual Threads могут выполнять свой код Java в одном и том же потоке ОС, фактически разделяя его. Платформенный поток монопольно занимает ценный поток ОС, а поток Virtual Threads — нет. Число потоков Virtual Threads может быть намного больше числа потоков ОС.
Virtual Threads — это легковесная реализация потоков, которую предоставляет JDK, а не ОС. Они представляют собой разновидность потоков пользовательского режима, которые успешно применяются в других многопоточных языках (например, горутины в Go и процессы в Erlang). Потоки пользовательского режима даже присутствовали в ранних версиях Java под названием «зелёные потоки», когда потоки ОС ещё не были зрелыми и широко распространёнными. Однако все зелёные потоки Java разделяли один поток ОС (планирование M:1), и в итоге платформенные потоки, реализованные как обёртки над потоками ОС (планирование 1:1), превзошли их по производительности. Virtual Threads используют планирование M:N, при котором большое число (M) потоков Virtual Threads планируется к выполнению на меньшем числе (N) потоков ОС.
Использование потоков Virtual Threads и платформенных потоков
Разработчики могут выбирать, использовать потоки Virtual Threads или платформенные потоки. Вот пример программы, которая создаёт большое число потоков Virtual Threads. Сначала программа получает ExecutorService, который будет создавать новый поток Virtual Threads для каждой отправленной задачи. Затем она отправляет 10 000 задач и ждёт завершения их всех:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
} // executor.close() is called implicitly, and waits
Задача в этом примере — простой код (заснуть на одну секунду), и современное оборудование легко выдерживает 10 000 потоков Virtual Threads, одновременно выполняющих такой код. За кулисами JDK выполняет этот код на небольшом числе потоков ОС, возможно всего на одном.
Всё было бы совсем иначе, если бы эта программа использовала ExecutorService, создающий новый платформенный поток для каждой задачи, например Executors.newCachedThreadPool(). ExecutorService попытался бы создать 10 000 платформенных потоков, а значит, и 10 000 потоков ОС, и программа могла бы аварийно завершиться — в зависимости от машины и операционной системы.
Ненамного лучше было бы, если бы программа вместо этого использовала ExecutorService, получающий платформенные потоки из пула, например Executors.newFixedThreadPool(200). ExecutorService создал бы 200 платформенных потоков, общих для всех 10 000 задач, поэтому многие задачи выполнялись бы последовательно, а не одновременно, и программа работала бы долго. Для этой программы пул из 200 платформенных потоков может обеспечить пропускную способность лишь 200 задач в секунду, тогда как потоки Virtual Threads достигают пропускной способности около 10 000 задач в секунду (после достаточного прогрева). Более того, если в примере заменить 10_000 на 1_000_000, программа отправит 1 000 000 задач, создаст 1 000 000 одновременно работающих потоков Virtual Threads и (после достаточного прогрева) достигнет пропускной способности около 1 000 000 задач в секунду.
Если бы задачи в этой программе не просто спали, а в течение одной секунды выполняли вычисления (например, сортировку огромного массива), то увеличение числа потоков сверх числа процессорных ядер не помогло бы, будь то потоки Virtual Threads или платформенные потоки. Virtual Threads — не более быстрые потоки: они выполняют код ничуть не быстрее платформенных потоков. Они существуют ради масштаба (более высокой пропускной способности), а не скорости (меньшей задержки). Их может быть гораздо больше, чем платформенных потоков, поэтому они обеспечивают более высокую конкурентность, необходимую для более высокой пропускной способности согласно закону Литтла.
Иначе говоря, Virtual Threads могут значительно повысить пропускную способность приложения, когда
- Число одновременно выполняемых задач велико (больше нескольких тысяч) и
- Нагрузка не упирается в процессор, поскольку в этом случае число потоков, намного превышающее число процессорных ядер, не может повысить пропускную способность.
Virtual Threads помогают повысить пропускную способность типичных серверных приложений именно потому, что такие приложения состоят из огромного числа одновременно выполняемых задач, которые большую часть времени проводят в ожидании.
Поток Virtual Threads может выполнять любой код, который может выполнять платформенный поток. В частности, Virtual Threads поддерживают локальные переменные потока и прерывание потоков, так же как платформенные потоки. Это значит, что существующий код Java, обрабатывающий запросы, будет без труда работать в потоке Virtual Threads. Многие серверные фреймворки предпочтут делать это автоматически, запуская новый поток Virtual Threads для каждого входящего запроса и выполняя в нём бизнес-логику приложения.
Вот пример серверного приложения, которое объединяет результаты двух других сервисов. Гипотетический серверный фреймворк (не показан) создаёт новый поток Virtual Threads для каждого запроса и выполняет в этом потоке код handle приложения. Код приложения, в свою очередь, создаёт два новых потока Virtual Threads, чтобы одновременно получить ресурсы через тот же ExecutorService, что и в первом примере:
void handle(Request request, Response response) {
var url1 = ...
var url2 = ...
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var future1 = executor.submit(() -> fetchURL(url1));
var future2 = executor.submit(() -> fetchURL(url2));
response.send(future1.get() + future2.get());
} catch (ExecutionException | InterruptedException e) {
response.fail(e);
}
}
String fetchURL(URL url) throws IOException {
try (var in = url.openStream()) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
Такое серверное приложение с простым блокирующим кодом хорошо масштабируется, поскольку может использовать большое число потоков Virtual Threads.
Executor.newVirtualThreadPerTaskExecutor() — не единственный способ создавать потоки Virtual Threads. Новый API java.lang.Thread.Builder, описанный ниже, позволяет создавать и запускать потоки Virtual Threads. Кроме того, Structured Concurrency (структурированная конкурентность) предлагает более мощный API для создания потоков Virtual Threads и управления ими, особенно в коде, подобном этому серверному примеру, где отношения между потоками становятся известны платформе и её инструментам.
Не объединяйте потоки Virtual Threads в пулы
Обычно разработчики переводят код приложения с традиционного ExecutorService на основе пула потоков на ExecutorService с отдельным потоком Virtual Threads на каждую задачу. Пул потоков, как и любой пул ресурсов, предназначен для совместного использования дорогих ресурсов, но потоки Virtual Threads недороги, поэтому объединять их в пул никогда не требуется.
Иногда разработчики используют пулы потоков, чтобы ограничить одновременный доступ к ограниченным ресурсам. Например, если сервис не может обрабатывать больше 20 одновременных запросов, то это можно гарантировать, выполняя все запросы к сервису через задачи, отправляемые в пул размером 20 потоков. Эта идиома стала повсеместной, потому что высокая стоимость платформенных потоков сделала повсеместными пулы потоков, но не поддавайтесь искушению объединять потоки Virtual Threads в пулы, чтобы ограничить конкурентность. Вместо этого используйте конструкции, специально предназначенные для этой цели, например семафоры.
Вместе с пулами потоков разработчики иногда используют локальные переменные потока, чтобы совместно использовать дорогие ресурсы в нескольких задачах, работающих в одном и том же потоке. Например, если соединение с базой данных дорого создавать, можно открыть его один раз и сохранить в локальной переменной потока, чтобы позже его использовали другие задачи в том же потоке. Если вы переводите код с пула потоков на отдельный поток Virtual Threads для каждой задачи, будьте осторожны с этой идиомой, поскольку создание дорогого ресурса для каждого потока Virtual Threads может значительно снизить производительность. Измените такой код так, чтобы он использовал альтернативные стратегии кэширования, позволяющие эффективно разделять дорогие ресурсы между очень большим числом потоков Virtual Threads.
Наблюдение за потоками Virtual Threads
Написать понятный код — это ещё не всё. Для устранения неполадок, сопровождения и оптимизации также необходимо наглядное представление состояния работающей программы, и JDK давно предлагает механизмы для отладки, профилирования и мониторинга потоков. Такие инструменты должны делать то же самое и для потоков Virtual Threads — возможно, с некоторой поправкой на их большое количество, — ведь это, в конце концов, экземпляры java.lang.Thread.
Отладчики Java могут выполнять код потоков Virtual Threads по шагам, показывать стеки вызовов и просматривать переменные во фреймах стека. JDK Flight Recorder (JFR) — механизм профилирования и мониторинга JDK с низкими накладными расходами — может связывать события из кода приложения (например, выделение памяти под объекты и операции ввода-вывода) с правильным потоком Virtual Threads. Для приложений, написанных в асинхронном стиле, эти инструменты этого сделать не могут. В этом стиле задачи не связаны с потоками, поэтому отладчики не могут показать состояние задачи или управлять им, а профилировщики не могут определить, сколько времени задача проводит в ожидании ввода-вывода.
Дамп потоков — ещё один популярный инструмент для устранения неполадок в приложениях, написанных в стиле «поток на запрос». К сожалению, традиционный дамп потоков JDK, получаемый с помощью jstack или jcmd, представляет собой плоский список потоков. Это подходит для десятков или сотен платформенных потоков, но не подходит для тысяч или миллионов потоков Virtual Threads. Поэтому мы не будем расширять традиционные дампы потоков, чтобы включить в них потоки Virtual Threads; вместо этого мы введём в jcmd новый вид дампа потоков, который показывает потоки Virtual Threads вместе с платформенными потоками, сгруппированными осмысленным образом. Когда программы используют Structured Concurrency, можно показать более богатые отношения между потоками.
Поскольку визуализации и анализу огромного числа потоков могут помочь специальные инструменты, jcmd может выводить новый дамп потоков не только в виде простого текста, но и в формате JSON:
$ jcmd <pid> Thread.dump_to_file -format=json <file>
Новый формат дампа потоков не включает адреса объектов, блокировки, статистику JNI, статистику кучи и другие сведения, которые есть в традиционных дампах потоков. Кроме того, поскольку в нём может потребоваться перечислить огромное число потоков, создание нового дампа потоков не приостанавливает приложение.
Если системному свойству jdk.trackAllThreads задано значение false, т. е. с помощью параметра командной строки -Djdk.trackAllThreads=false, то потоки Virtual Threads, созданные напрямую через API Thread.Builder, не всегда будут отслеживаться средой выполнения и могут не появиться в новом дампе потоков. В этом случае новый дамп потоков будет содержать потоки Virtual Threads, заблокированные в сетевых операциях ввода-вывода, и потоки Virtual Threads, созданные показанным выше ExecutorService с новым потоком на каждую задачу.
Вот пример такого дампа потоков, снятого с приложения, похожего на второй пример выше, в программе просмотра JSON (нажмите, чтобы увеличить):
Поскольку Virtual Threads реализованы в JDK и не привязаны ни к какому конкретному потоку ОС, они невидимы для ОС, которая не знает об их существовании. Мониторинг на уровне ОС покажет, что процесс JDK использует меньше потоков ОС, чем существует потоков Virtual Threads.
Планирование потоков Virtual Threads
Чтобы выполнять полезную работу, поток должен быть запланирован, то есть назначен на выполнение на процессорном ядре. Для платформенных потоков, которые реализованы как потоки ОС, JDK полагается на планировщик ОС. Для потоков Virtual Threads, напротив, у JDK есть собственный планировщик. Вместо того чтобы назначать потоки Virtual Threads непосредственно процессорам, планировщик JDK назначает их платформенным потокам (это и есть упомянутое ранее планирование M:N для Virtual Threads). Затем платформенные потоки планируются ОС как обычно.
Планировщик потоков Virtual Threads в JDK — это ForkJoinPool с перехватом работы (work-stealing), работающий в режиме FIFO. Параллелизм планировщика — это число платформенных потоков, доступных для планирования потоков Virtual Threads. По умолчанию он равен числу доступных процессоров, но его можно настроить системным свойством jdk.virtualThreadScheduler.parallelism. Этот ForkJoinPool отличается от общего пула, который используется, например, в реализации параллельных потоков данных (parallel streams) и работает в режиме LIFO.
Платформенный поток, которому планировщик назначает поток Virtual Threads, называется носителем (carrier) этого потока Virtual Threads. За время своей жизни поток Virtual Threads может планироваться на разных носителях; иными словами, планировщик не поддерживает привязку (affinity) между потоком Virtual Threads и каким-либо конкретным платформенным потоком. С точки зрения кода Java работающий поток Virtual Threads логически независим от своего текущего носителя:
-
Потоку Virtual Threads недоступны сведения о том, какой поток является его носителем. Значение, возвращаемое
Thread.currentThread(), — всегда сам поток Virtual Threads. -
Трассировки стека носителя и потока Virtual Threads разделены. Исключение, выброшенное в потоке Virtual Threads, не будет включать фреймы стека носителя. Дампы потоков не будут показывать фреймы стека носителя в стеке потока Virtual Threads, и наоборот.
-
Локальные переменные потока, принадлежащие носителю, недоступны потоку Virtual Threads, и наоборот.
Кроме того, с точки зрения кода Java невидимо то, что поток Virtual Threads и его носитель временно разделяют поток ОС. С точки зрения нативного кода, напротив, и поток Virtual Threads, и его носитель выполняются в одном и том же нативном потоке. Поэтому нативный код, который вызывается несколько раз в одном и том же потоке Virtual Threads, может при каждом вызове видеть разный идентификатор потока ОС.
В настоящее время планировщик не реализует для потоков Virtual Threads разделение времени. Разделение времени — это принудительное вытеснение потока, израсходовавшего выделенную ему долю процессорного времени. Разделение времени может помогать снизить задержку некоторых задач, когда платформенных потоков относительно немного, а загрузка процессора составляет 100 %. Однако неясно, будет ли оно так же эффективно при миллионе потоков Virtual Threads.
Выполнение потоков Virtual Threads
Чтобы воспользоваться потоками Virtual Threads, переписывать программу не нужно. Потоки Virtual Threads не требуют и не ожидают, что код приложения будет явно возвращать управление планировщику; иными словами, потоки Virtual Threads не являются кооперативными. Пользовательский код не должен делать предположений о том, как и когда потоки Virtual Threads назначаются платформенным потокам. Точно так же он не делает предположений о том, как и когда платформенные потоки назначаются ядрам процессора.
Чтобы выполнить код в потоке Virtual Threads, планировщик потоков Virtual Threads в JDK назначает этот поток на выполнение в платформенном потоке, монтируя его на платформенный поток. В результате платформенный поток становится носителем потока Virtual Threads. Позже, выполнив какой-то код, поток Virtual Threads может размонтироваться со своего носителя. В этот момент платформенный поток освобождается, и планировщик может смонтировать на него другой поток Virtual Threads, снова делая его носителем.
Как правило, поток Virtual Threads размонтируется, когда блокируется на вводе-выводе или на другой блокирующей операции в JDK, например BlockingQueue.take(). Когда блокирующая операция готова завершиться (например, из сокета получены байты), она возвращает поток Virtual Threads планировщику, а тот монтирует поток Virtual Threads на носитель, чтобы продолжить выполнение.
Монтирование и размонтирование потоков Virtual Threads происходит часто и незаметно, без блокировки каких-либо потоков ОС. Например, в показанном ранее серверном приложении была следующая строка кода, которая содержит вызовы блокирующих операций:
response.send(future1.get() + future2.get());
Эти операции заставят поток Virtual Threads монтироваться и размонтироваться несколько раз: как правило, по одному разу при каждом вызове get() и, возможно, несколько раз в ходе выполнения ввода-вывода в send(...).
Подавляющее большинство блокирующих операций в JDK размонтирует поток Virtual Threads, освобождая его носитель и нижележащий поток ОС для новой работы. Однако некоторые блокирующие операции в JDK не размонтируют поток Virtual Threads и поэтому блокируют и его носитель, и нижележащий поток ОС. Причина — ограничения либо на уровне ОС (например, многие операции с файловой системой), либо на уровне JDK, например Object.wait(). Реализации этих блокирующих операций компенсируют захват потока ОС, временно увеличивая параллелизм планировщика. Поэтому число платформенных потоков в ForkJoinPool планировщика может временно превышать число доступных процессоров. Максимальное число платформенных потоков, доступных планировщику, можно настроить системным свойством jdk.virtualThreadScheduler.maxPoolSize.
Есть два сценария, в которых поток Virtual Threads не может быть размонтирован во время блокирующих операций, потому что он привязан к своему носителю:
- когда он выполняет код внутри блока или метода
synchronizedили - когда он выполняет метод
nativeили внешнюю функцию.
Pinning (привязка виртуального потока к платформенному) не делает приложение некорректным, но может мешать его масштабируемости. Если привязанный поток Virtual Threads выполняет блокирующую операцию, например ввод-вывод или BlockingQueue.take(), то его носитель и нижележащий поток ОС блокируются на всё время операции. Частый и длительный Pinning может ухудшить масштабируемость приложения, так как захватывает носители.
Планировщик не компенсирует Pinning увеличением параллелизма. Вместо этого избегайте частого и длительного Pinning: переработайте часто выполняемые блоки или методы synchronized, которые защищают потенциально долгие операции ввода-вывода, так, чтобы они использовали java.util.concurrent.locks.ReentrantLock. Заменять блоки и методы synchronized, которые используются редко (например, выполняются только при запуске) или защищают операции в памяти, не нужно. Как всегда, старайтесь, чтобы политики блокировок были простыми и понятными.
Новые средства диагностики помогают переводить код на потоки Virtual Threads и оценивать, стоит ли заменить конкретное использование synchronized блокировкой java.util.concurrent:
-
Когда поток блокируется в привязанном состоянии, генерируется событие JDK Flight Recorder (JFR) (см. JDK Flight Recorder).
-
Системное свойство
jdk.tracePinnedThreadsвключает вывод трассировки стека, когда поток блокируется в привязанном состоянии. При запуске с-Djdk.tracePinnedThreads=fullвыводится полная трассировка стека, когда поток блокируется в привязанном состоянии, при этом выделяются нативные фреймы и фреймы, удерживающие мониторы. При запуске с-Djdk.tracePinnedThreads=shortвыводятся только проблемные фреймы.
В одном из будущих выпусков мы, возможно, сможем снять первое из указанных выше ограничений, а именно Pinning внутри synchronized. Второе ограничение необходимо для корректного взаимодействия с нативным кодом.
Использование памяти и взаимодействие со сборкой мусора
Стеки потоков Virtual Threads хранятся в куче Java, которую обслуживает сборщик мусора, в виде объектов stack chunk (фрагментов стека). Стеки растут и сжимаются по ходу работы приложения: так они экономно расходуют память и при этом вмещают стеки глубиной вплоть до размера стека платформенного потока, заданного в настройках JVM. Именно эта эффективность позволяет иметь большое число потоков Virtual Threads и тем самым сохраняет жизнеспособность стиля «поток на запрос» в серверных приложениях.
Вспомним, что во втором примере выше гипотетический фреймворк обрабатывает каждый запрос, создавая новый поток Virtual Threads и вызывая метод handle. Даже если он вызывает handle в конце глубокого стека вызовов (после аутентификации, транзакций и т. д.), сам handle порождает несколько потоков Virtual Threads, которые выполняют только короткие задачи. Поэтому на каждый поток Virtual Threads с глубоким стеком вызовов придётся несколько потоков Virtual Threads с неглубокими стеками вызовов, потребляющих мало памяти.
Объём памяти в куче и активность сборщика мусора, которые требуются потокам Virtual Threads, в общем случае трудно сравнить с тем, что требуется асинхронному коду. Миллиону потоков Virtual Threads нужен как минимум миллион объектов, но столько же нужно и миллиону задач, разделяющих пул платформенных потоков. Кроме того, код приложения, обрабатывающий запросы, обычно хранит данные между операциями ввода-вывода. Код в стиле «поток на запрос» может держать эти данные в локальных переменных, которые хранятся в стеках потоков Virtual Threads в куче, а асинхронный код должен держать те же данные в объектах кучи, которые передаются от одной стадии конвейера к следующей. С одной стороны, раскладка фреймов стека, нужная потокам Virtual Threads, расходует память менее экономно, чем компактный объект; с другой стороны, потоки Virtual Threads во многих ситуациях могут изменять и повторно использовать свои стеки (в зависимости от низкоуровневого взаимодействия со сборщиком мусора), тогда как асинхронным конвейерам всегда нужно выделять новые объекты, поэтому потокам Virtual Threads может требоваться меньше выделений памяти. В целом потребление кучи и активность сборщика мусора у кода в стиле «поток на запрос» и у асинхронного кода должны быть примерно одинаковыми. Со временем мы рассчитываем сделать внутреннее представление стеков потоков Virtual Threads значительно компактнее.
В отличие от стеков платформенных потоков, стеки потоков Virtual Threads не являются корнями GC. Поэтому сборщики мусора, выполняющие конкурентное сканирование кучи, такие как G1, не обходят содержащиеся в них ссылки во время паузы stop-the-world.
Текущее ограничение потоков Virtual Threads состоит в том, что G1 GC не поддерживает объекты stack chunk категории humongous. Если стек потока Virtual Threads достигает половины размера региона, который может быть всего 512 КБ, может быть выброшено StackOverflowError.
Подробное описание изменений
В остальных подразделах подробно описаны изменения, которые мы предлагаем внести в платформу Java и её реализацию:
java.lang.Thread- Локальные переменные потока
java.util.concurrent- Сетевое взаимодействие
java.io- Java Native Interface (JNI)
- Отладка (JVM TI, JDWP и JDI)
- JDK Flight Recorder (JFR)
- Java Management Extensions (JMX)
java.lang.Thread
Мы изменяем API java.lang.Thread следующим образом:
-
Thread.Builder,Thread.ofVirtual()иThread.ofPlatform()— новые API для создания потоков Virtual Threads и платформенных потоков. Например,Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);создаёт новый незапущенный поток Virtual Threads с именем
"duke". -
Thread.startVirtualThread(Runnable)— удобный способ создать и сразу запустить поток Virtual Threads. -
Thread.Builderможет создать либо поток, либоThreadFactory, который затем может создавать несколько потоков с одинаковыми свойствами. -
Thread.isVirtual()проверяет, является ли поток потоком Virtual Threads. -
Thread.getAllStackTraces()теперь возвращает отображение всех платформенных потоков, а не всех потоков.
В остальном этот JEP не меняет API java.lang.Thread. Конструкторы, определённые в классе Thread, как и раньше, создают платформенные потоки. Новых публичных конструкторов нет.
(Три метода в Thread, которые выбрасывают UnsupportedOperationException для потоков Virtual Threads, — stop(), suspend() и resume() — были изменены в JDK 20 так, чтобы выбрасывать UnsupportedOperationException и для платформенных потоков.)
Основные различия в API между потоками Virtual Threads и платформенными потоками:
-
Публичные конструкторы
Threadне могут создавать потоки Virtual Threads. -
Потоки Virtual Threads всегда являются потоками-демонами. Метод
Thread.setDaemon(boolean)не может сделать поток Virtual Threads потоком, не являющимся демоном. -
У потоков Virtual Threads фиксированный приоритет
Thread.NORM_PRIORITY. МетодThread.setPriority(int)не влияет на потоки Virtual Threads. Это ограничение может быть пересмотрено в одном из будущих выпусков. -
Потоки Virtual Threads не являются активными членами групп потоков. При вызове для потока Virtual Threads метод
Thread.getThreadGroup()возвращает группу-заглушку с именем"VirtualThreads". APIThread.Builderне определяет метода для задания группы потоков потоку Virtual Threads. -
При работе с установленным
SecurityManagerу потоков Virtual Threads нет никаких разрешений.
Локальные переменные потока
Потоки Virtual Threads поддерживают локальные переменные потока (ThreadLocal) и наследуемые локальные переменные потока (InheritableThreadLocal) так же, как платформенные потоки, поэтому они могут выполнять существующий код, использующий локальные переменные потока. Однако, поскольку потоков Virtual Threads может быть очень много, используйте локальные переменные потока только после тщательного обдумывания. В частности, не используйте локальные переменные потока для объединения в пул дорогостоящих ресурсов между несколькими задачами, разделяющими один и тот же поток в пуле потоков. Потоки Virtual Threads никогда не следует объединять в пул, поскольку каждый из них предназначен для выполнения только одной задачи за всё время своей жизни. Готовясь к появлению потоков Virtual Threads, мы убрали многие использования локальных переменных потока из модуля java.base в JDK, чтобы уменьшить расход памяти при работе с миллионами потоков.
Системное свойство jdk.traceVirtualThreadLocals можно использовать, чтобы выводить трассировку стека, когда поток Virtual Threads задаёт значение любой локальной переменной потока. Этот диагностический вывод может помочь избавиться от локальных переменных потока при переводе кода на потоки Virtual Threads. Чтобы включить вывод трассировок стека, задайте системному свойству значение true; значение по умолчанию — false.
Для некоторых сценариев использования Scoped Values (значения с ограниченной областью видимости) (JEP 429) могут оказаться лучшей альтернативой локальным переменным потока.
java.util.concurrent
Примитивный API для поддержки блокировок, java.util.concurrent.LockSupport, теперь поддерживает потоки Virtual Threads: парковка потока Virtual Threads освобождает нижележащий платформенный поток для другой работы, а распарковка потока Virtual Threads планирует его продолжение. Это изменение в LockSupport позволяет всем использующим его API (Lock, Semaphore, блокирующим очередям и т. д.) корректно выполнять парковку при вызове в потоках Virtual Threads.
Кроме того, Executors.newThreadPerTaskExecutor(ThreadFactory) и Executors.newVirtualThreadPerTaskExecutor() создают ExecutorService, который создаёт новый поток для каждой задачи. Эти методы обеспечивают переход и совместимость с существующим кодом, использующим пулы потоков и ExecutorService.
Сетевое взаимодействие
Реализации сетевых API в пакетах java.net и java.nio.channels теперь работают с потоками Virtual Threads: блокирующая операция в потоке Virtual Threads, например установка сетевого соединения или чтение из сокета, освобождает нижележащий платформенный поток для другой работы.
Чтобы можно было прерывать и отменять операции, блокирующие методы ввода-вывода, определённые в java.net.Socket, ServerSocket и DatagramSocket, теперь по спецификации являются прерываемыми при вызове в потоке Virtual Threads: прерывание потока Virtual Threads, заблокированного на сокете, распаркует поток и закроет сокет. Блокирующие операции ввода-вывода на сокетах этих типов, полученных из InterruptibleChannel, всегда были прерываемыми, поэтому это изменение согласует поведение этих API при создании через конструкторы с их поведением при получении из канала.
java.io
Пакет java.io предоставляет API для потоков байтов и символов. Реализации этих API активно используют синхронизацию и требуют изменений, чтобы избежать Pinning при использовании в потоках Virtual Threads.
Для справки: по спецификации байтовые потоки ввода-вывода не обязаны быть потокобезопасными, и спецификация не определяет ожидаемое поведение при вызове close(), пока поток заблокирован в методе чтения или записи. В большинстве сценариев нет смысла использовать один и тот же поток ввода или вывода из нескольких конкурентных потоков. Символьные reader/writer по спецификации также не обязаны быть потокобезопасными, но они предоставляют объект блокировки для подклассов. Помимо Pinning, синхронизация в этих классах проблематична и непоследовательна; например, декодеры и кодировщики потоков, используемые InputStreamReader и OutputStreamWriter, синхронизируются на объекте потока, а не на объекте блокировки.
Чтобы предотвратить Pinning, реализации теперь работают так:
-
BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter,PrintStreamиPrintWriterпри прямом использовании теперь применяют явную блокировку, а не монитор. Если от этих классов наследуются, они синхронизируются как прежде. -
Декодеры и кодировщики потоков, используемые
InputStreamReaderиOutputStreamWriter, теперь используют ту же блокировку, что и охватывающийInputStreamReaderилиOutputStreamWriter.
Дальнейшие шаги по устранению всех этих зачастую ненужных блокировок выходят за рамки этого JEP.
Кроме того, начальные размеры буферов, используемых BufferedOutputStream, BufferedWriter и кодировщиком потока для OutputStreamWriter, теперь уменьшены, чтобы сократить расход памяти, когда в куче много потоков ввода-вывода или writer-объектов, — как может случиться, если есть миллион потоков Virtual Threads, у каждого из которых буферизованный поток на сокетном соединении.
Java Native Interface (JNI)
JNI определяет одну новую функцию, IsVirtualThread, которая проверяет, является ли объект потоком Virtual Threads.
В остальном спецификация JNI не изменилась.
Отладка
Архитектура отладки состоит из трёх интерфейсов: JVM Tool Interface (JVM TI), Java Debug Wire Protocol (JDWP) и Java Debug Interface (JDI). Все три интерфейса теперь поддерживают потоки Virtual Threads.
Изменения в JVM TI:
-
Большинство функций, вызываемых с
jthread(то есть JNI-ссылкой на объектThread), можно вызывать со ссылкой на поток Virtual Threads. Небольшое число функций, а именноAgentStartFunction,PopFrame,ForceEarlyReturn*,StopThreadиGetThreadCpuTime, не поддерживаются или поддерживаются опционально для потоков Virtual Threads. ФункцииSetLocal*ограничены установкой локальных переменных в самых верхних фреймах потоков Virtual Threads, приостановленных на точке останова или событии пошагового выполнения. -
Функции
GetAllThreadsиGetAllStackTracesтеперь по спецификации возвращают все платформенные потоки, а не все потоки. -
Для всех событий, кроме публикуемых на раннем этапе запуска VM или во время обхода кучи, обратные вызовы событий могут выполняться в контексте потока Virtual Threads.
-
Реализация приостановки и возобновления позволяет отладчикам приостанавливать и возобновлять потоки Virtual Threads, а также позволяет приостанавливать платформенные потоки, когда на них смонтирован поток Virtual Threads.
-
Новая возможность,
can_support_virtual_threads, даёт агентам более точный контроль над событиями запуска и завершения потоков Virtual Threads. -
Новые функции поддерживают массовую приостановку и возобновление потоков Virtual Threads; для них требуется возможность
can_support_virtual_threads.
Существующие агенты JVM TI в основном будут работать как прежде, но могут столкнуться с ошибками, если вызывают функции, не поддерживаемые для потоков Virtual Threads. Это произойдёт, когда агент, не учитывающий потоки Virtual Threads, используется с приложением, которое использует потоки Virtual Threads. Изменение GetAllThreads, которая теперь возвращает массив только с платформенными потоками, может стать проблемой для некоторых агентов. Существующие агенты, включающие события ThreadStart и ThreadEnd, могут столкнуться с проблемами производительности, поскольку не могут ограничить эти события платформенными потоками.
Изменения в JDWP:
-
Новая команда позволяет отладчикам проверить, является ли поток потоком Virtual Threads.
-
Новый модификатор команды
EventRequestпозволяет отладчикам ограничить события запуска и завершения потоков платформенными потоками.
Изменения в JDI:
-
Новый метод в
com.sun.jdi.ThreadReferenceпроверяет, является ли поток потоком Virtual Threads. -
Новые методы в
com.sun.jdi.request.ThreadStartRequestиcom.sun.jdi.request.ThreadDeathRequestограничивают события, генерируемые для запроса, платформенными потоками.
Как отмечено выше, потоки Virtual Threads не считаются активными потоками в группе потоков. Поэтому списки потоков, возвращаемые функцией JVM TI GetThreadGroupChildren, командой JDWP ThreadGroupReference/Children и методом JDI com.sun.jdi.ThreadGroupReference.threads(), содержат только платформенные потоки.
JDK Flight Recorder (JFR)
JFR поддерживает потоки Virtual Threads с помощью нескольких новых событий:
-
jdk.VirtualThreadStartиjdk.VirtualThreadEndобозначают запуск и завершение потока Virtual Threads. По умолчанию эти события отключены. -
jdk.VirtualThreadPinnedобозначает, что поток Virtual Threads был запаркован в состоянии Pinning, то есть без освобождения своего платформенного потока (см. выше). Это событие включено по умолчанию с порогом 20 мс. -
jdk.VirtualThreadSubmitFailedобозначает, что запуск или распарковка потока Virtual Threads не удались, вероятно из-за нехватки ресурсов. Это событие включено по умолчанию.
Java Management Extensions (JMX)
java.lang.management.ThreadMXBean поддерживает мониторинг и управление только платформенными потоками. Метод findDeadlockedThreads() находит циклы платформенных потоков, находящихся во взаимоблокировке; циклы потоков Virtual Threads во взаимоблокировке он не находит.
Новый метод в com.sun.management.HotSpotDiagnosticsMXBean создаёт дамп потоков в новом формате, описанном выше. Этот метод также можно вызвать косвенно через платформенный MBeanServer из локального или удалённого инструмента JMX.
Альтернативы
-
Продолжать полагаться на асинхронные API. Асинхронные API трудно интегрировать с синхронными API, они разделяют мир на два представления одних и тех же операций ввода-вывода и не дают единого понятия последовательности операций, которое платформа могла бы использовать как контекст для диагностики неполадок, мониторинга, отладки и профилирования.
-
Добавить в язык Java синтаксические бесстековые корутины (то есть async/await). Их проще реализовать, чем потоки пользовательского режима, и они дали бы единую конструкцию, представляющую контекст последовательности операций.
Однако эта конструкция была бы новой и отдельной от потоков, во многом похожей на них, но в некоторых тонкостях отличающейся. Она разделила бы мир на API, спроектированные для потоков, и API, спроектированные для корутин, и потребовала бы внедрить новую потокоподобную конструкцию во все уровни платформы и её инструментов. Экосистеме понадобилось бы больше времени на её освоение, и она не была бы столь элегантной и гармоничной с платформой, как потоки пользовательского режима.
Большинство языков, которые приняли синтаксические корутины, сделали это из-за невозможности реализовать потоки пользовательского режима (например, Kotlin), унаследованных семантических гарантий (например, изначально однопоточный JavaScript) или технических ограничений конкретного языка (например, C++). К Java эти ограничения не относятся.
-
Ввести новый публичный класс для представления потоков пользовательского режима, не связанный с
java.lang.Thread. Это дало бы возможность избавиться от нежелательного багажа, который классThreadнакопил за 25 лет. Мы исследовали несколько вариантов этого подхода и создали их прототипы, но в каждом случае сталкивались с вопросом, как выполнять существующий код.Главная проблема в том, что
Thread.currentThread()повсеместно используется в существующем коде, прямо или косвенно (например, для определения владельца блокировки или для потоково-локальных переменных). Этот метод должен возвращать объект, представляющий текущий поток выполнения. Если бы мы ввели новый класс для представления потоков пользовательского режима,currentThread()пришлось бы возвращать какой-то объект-обёртку, который выглядит какThread, но делегирует объекту потока пользовательского режима.Два объекта, представляющих текущий поток выполнения, приводили бы к путанице, поэтому в итоге мы пришли к выводу, что сохранение старого API
Threadне является серьёзным препятствием. За исключением нескольких методов, таких какcurrentThread(), разработчики редко используют APIThreadнапрямую; в основном они работают с API более высокого уровня, такими какExecutorService. Со временем мы избавимся от нежелательного багажа классаThreadи связанных классов, таких какThreadGroup, помечая устаревшие методы как Deprecated (устаревший) и удаляя их.
Тестирование
-
Существующие тесты обеспечат, что предлагаемые здесь изменения не вызовут неожиданных регрессий во множестве конфигураций и режимов выполнения, в которых эти тесты запускаются.
-
Мы расширим тестовую среду
jtreg, чтобы существующие тесты можно было запускать в контексте потока Virtual Threads. Так не понадобятся две версии многих тестов. -
Новые тесты будут проверять все новые и изменённые API, а также все области, изменённые для поддержки потоков Virtual Threads.
-
Новые стресс-тесты будут нацелены на области, критически важные для надёжности и производительности.
-
Новые микробенчмарки будут нацелены на области, критически важные для производительности.
-
Для более масштабного тестирования мы будем использовать ряд существующих серверов, включая Helidon и Jetty.
Риски и допущения
Основные риски этого предложения связаны с совместимостью из-за изменений в существующих API и их реализациях:
- Изменения внутреннего (и недокументированного) протокола блокировки, используемого в классах
java.io.BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter,PrintStreamиPrintWriter, могут повлиять на код, который предполагает, что методы ввода-вывода синхронизируются на потоке, для которого они вызваны. Эти изменения не влияют на код, который расширяет эти классы и рассчитывает на блокировку в суперклассе, а также на код, который расширяетjava.io.Readerилиjava.io.Writerи использует объект блокировки, предоставляемый этими API.
Несколько изменений, несовместимых на уровне исходного кода и бинарном уровне, могут повлиять на код, расширяющий java.lang.Thread:
-
Threadопределяет несколько новых методов. Если код в существующем исходном файле расширяетThreadи метод подкласса конфликтует с каким-либо из новых методовThread, файл не скомпилируется без изменений. -
Thread.Builder— новый вложенный интерфейс. Если код в существующем исходном файле расширяетThread, импортирует класс с именемBuilderи код подкласса ссылается наBuilderпо простому имени, файл не скомпилируется без изменений. -
Thread.isVirtual()— новый final-метод. Если существующий скомпилированный код расширяетThreadи подкласс объявляет метод с тем же именем и типом возвращаемого значения, то при загрузке подкласса во время выполнения будет выброшеноIncompatibleClassChangeError.
При сочетании существующего кода с новым кодом, использующим потоки Virtual Threads или новые API, могут наблюдаться некоторые различия в поведении платформенных потоков и потоков Virtual Threads:
-
Метод
Thread.setPriority(int)не действует на потоки Virtual Threads, у которых приоритет всегдаThread.NORM_PRIORITY. -
Метод
Thread.setDaemon(boolean)не действует на потоки Virtual Threads, которые всегда являются потоками-демонами. -
Thread.getAllStackTraces()теперь возвращает отображение всех платформенных потоков, а не всех потоков. -
Блокирующие методы ввода-вывода, определённые в
java.net.Socket,ServerSocketиDatagramSocket, теперь можно прервать, если они вызваны в контексте потока Virtual Threads. Существующий код может сломаться, если прервать поток, заблокированный на операции с сокетом: поток будет разбужен, а сокет закрыт. -
Потоки Virtual Threads не являются активными членами
ThreadGroup. ВызовThread.getThreadGroup()для потока Virtual Threads возвращает фиктивную пустую группу"VirtualThreads". -
При работе с установленным менеджером безопасности у потоков Virtual Threads нет никаких разрешений. О работе с менеджером безопасности в Java 17 и более поздних версиях см. JEP 411 (Deprecate the Security Manager for Removal).
-
В JVM TI функции
GetAllThreadsиGetAllStackTracesне возвращают потоки Virtual Threads. У существующих агентов, которые включают событияThreadStartиThreadEnd, могут возникнуть проблемы с производительностью, так как они не могут ограничить эти события платформенными потоками. -
API
java.lang.management.ThreadMXBeanподдерживает мониторинг и управление платформенными потоками, но не потоками Virtual Threads. -
Флаг
-XX:+PreserveFramePointerрезко ухудшает производительность потоков Virtual Threads.
Зависимости
-
JEP 416 (Reimplement Core Reflection with Method Handles) в JDK 18 удалил нативную реализацию рефлексии в VM. Благодаря этому потоки Virtual Threads могут корректно приостанавливаться (park), когда методы вызываются через рефлексию.
-
JEP 353 (Reimplement the Legacy Socket API) в JDK 13 и JEP 373 (Reimplement the Legacy DatagramSocket API) в JDK 15 заменили реализации
java.net.Socket,ServerSocketиDatagramSocketновыми реализациями, рассчитанными на использование с потоками Virtual Threads. -
JEP 418 (Internet-Address Resolution SPI) в JDK 18 определил интерфейс поставщика услуг (service-provider interface) для поиска имён хостов и адресов. Так сторонние библиотеки смогут реализовывать альтернативные резолверы
java.net.InetAddress, которые не привязывают потоки во время поиска хоста.