Project Loom — это проект, разрабатываемый Oracle для Java, который добавляет лёгкие потоки или «виртуальные потоки» в язык. Цель Project Loom — упростить написание параллельного кода, позволив использовать асинхронное выполнение без явного использования сложных абстракций вроде потоков, коллбеков или корутин.
Основные особенности
Виртуальные потоки:
основное новшество Loom. Виртуальные потоки — это легковесные потоки, реализованные на уровне JVM, которые позволяют обрабатывать тысячи или даже миллионы задач параллельно.
Поддержка параллельных вызовов:
благодаря виртуальным потокам, можно писать блокирующий код (например, синхронные вызовы sleep, wait или I/O-операции), который будет при этом выполнять задачи параллельно.
Поддержка с JDK 19:
виртуальные потоки включены в качестве превью-функции, начиная с JDK 19. Ожидается, что в будущем они станут стандартом в JDK.
Преимущества
Упрощённый асинхронный код:
Код, написанный с виртуальными потоками, может быть линейным и блокирующим, не требует асинхронных API или сложных конструкций.
Уменьшенные накладные расходы:
Нет необходимости создавать реальные потоки ОС для каждой задачи, что снижает накладные расходы и позволяет JVM выполнять сотни тысяч виртуальных потоков на ограниченном количестве потоков ОС.
Масштабируемость:
Виртуальные потоки делают масштабируемыми приложения, обрабатывающие большое количество параллельных задач (например, высоконагруженные серверы).
Недостатки
Зависимость от JVM:
Loom работает только на JVM, что ограничивает его использование, если требуется совместимость с другой платформой или версией Java ниже JDK 19.
Ограниченная поддержка и стабильность:
Project Loom всё ещё находится на стадии разработки и требует дальнейших улучшений и оптимизации.
Отсутствие функциональности реактивных библиотек:
Loom не предлагает встроенных инструментов для реактивного программирования, таких как трансформации данных и операторы, которые есть в библиотеках типа RxJava.
Принцип работы
Создание виртуального потока:
Виртуальные потоки создаются аналогично обычным потокам Java (Thread), но они гораздо легче и не занимают значительного объёма системных ресурсов.
При создании виртуального потока JVM не выделяет ему поток ОС. Вместо этого он работает как задача, которую можно приостановить, запустить и снова приостановить, не занимая поток ОС постоянно.
Thread.startVirtualThread(() -> { // код, выполняющийся в виртуальном потоке });
Планировщик потоков JVM:
Виртуальные потоки управляются специальным планировщиком на уровне JVM, который распределяет их выполнение на реальные потоки ОС (каркасные потоки, «carrier threads»).
Планировщик действует подобно планировщикам ОС, но в пределах JVM, что позволяет более гибко управлять приостановкой и возобновлением виртуальных потоков.
Блокирующие операции и приостановка:
При выполнении блокирующей операции, такой как I/O или sleep, виртуальный поток автоматически приостанавливается JVM и освобождает ресурс каркасного потока.
При этом происходит переключение контекста, при котором виртуальный поток освобождает каркасный поток. Этот процесс значительно быстрее и менее ресурсоёмкий, чем работа с потоками ОС.
При завершении блокирующей операции виртуальный поток снова ставится в очередь для выполнения на одном из каркасных потоков.
Возобновление виртуального потока:
Когда блокирующая операция завершается, виртуальный поток снова назначается на выполнение и подхватывается каркасным потоком.
Это позволяет JVM выполнять сотни тысяч виртуальных потоков с ограниченным количеством потоков ОС, обеспечивая масштабируемость.
Управление стэком:
Одной из главных задач в реализации Project Loom было создание лёгких виртуальных потоков с поддержкой приостановки и возобновления, что потребовало модификаций стека.
При приостановке виртуального потока JVM сохраняет стек вызовов этого потока, что позволяет возобновить выполнение потока позже, как будто он никогда не приостанавливался.
Эффективное использование ресурсов:
Поскольку виртуальные потоки не привязаны к потокам ОС, JVM может запускать тысячи виртуальных потоков, используя всего несколько каркасных потоков.
Это позволяет эффективно использовать процессорное время и уменьшить накладные расходы, так как JVM не обращается к системным вызовам для создания новых потоков ОС.
Технические компоненты и аспекты
Виртуальные потоки (Virtual Threads):
это ключевой компонент Project Loom. Виртуальные потоки создаются и управляются JVM, а не ОС, и могут приостанавливаться и возобновляться по мере необходимости, освобождая ресурсы для других задач.
Каркасные потоки (Carrier Threads):
это потоки ОС, которые используются JVM для выполнения виртуальных потоков. JVM управляет пулом каркасных потоков, которые выполняют приостановленные виртуальные потоки.
Продвинутое управление стэком:
Loom использует копирование стека при приостановке, что позволяет сохранять текущее состояние выполнения потока и восстанавливать его при возобновлении.
Оптимизированное управление I/O:
Loom встроен в JVM таким образом, что блокирующие вызовы, такие как чтение из файлов или сетевые запросы, обрабатываются автоматически, приостанавливая виртуальные потоки до завершения I/O.
Совместимость с существующим кодом:
Виртуальные потоки выглядят как обычные потоки Thread для остальной части Java-программы, что позволяет использовать Loom с существующим кодом без изменений.
Совместимость
Project Loom поддерживается начиная с JDK 19 и далее. Виртуальные потоки появились как превью-функция, и с выходом каждой последующей версии JDK, начиная с 19, Project Loom постепенно обретает стабильность и улучшения. Проект Loom разрабатывается в рамках OpenJDK, поэтому его поддержка доступна только на JVM, совместимых с OpenJDK.
JDK 8 — JDK 18:
На этих версиях виртуальные потоки Loom не поддерживаются и не могут быть активированы. Для этих версий Java придётся использовать альтернативные подходы для асинхронного программирования, такие как стандартные потоки, CompletableFuture, RxJava, Kotlin корутины и другие.
Сторонние реализации JVM на базе JDK 8-18:
Даже если это реализация от стороннего вендора (например, Amazon Corretto, GraalVM, Zulu и т. д.), если она основана на JDK 8-18, то поддержка виртуальных потоков Project Loom также будет отсутствовать.
JDK 19 и выше:
Loom доступен для использования начиная с этой версии, но как превью, что требует явного включения флага для активации виртуальных потоков.
JDK 20 и 21:
в этих версиях Project Loom продолжает совершенствоваться, добавляя стабильность и улучшения производительности.
JVM от OpenJDK:
Project Loom разрабатывается в рамках OpenJDK, так что все реализации JVM, основанные на OpenJDK (например, Zulu, AdoptOpenJDK и другие), тоже поддерживают виртуальные потоки, если они используют JDK 19+.
Не-OpenJDK JVM:
Если JVM не основана на OpenJDK (например, IBM J9 или другие проприетарные реализации), поддержка Project Loom не гарантируется, даже если версия соответствует JDK 19 или выше. Такие JVM, вероятнее всего, не включат Loom, поскольку этот проект специфичен для OpenJDK и зависит от его внутренней реализации.
GraalVM:
GraalVM на базе OpenJDK 19+ поддерживает Project Loom, если используется OpenJDK-совместимая конфигурация. Однако, если это специализированная версия GraalVM, которая не полностью совместима с OpenJDK (например, Native Image), поддержка может быть ограничена или отсутствовать.
Проприетарные сборки, несовместимые с OpenJDK:
Некоторые специализированные сборки JVM могут не включать все функции, доступные в OpenJDK 19+, так что поддержка Loom также будет отсутствовать.
Отличия от Kotlin Coroutines
Модель исполнения:
Project Loom:
виртуальные потоки в Loom реализуются на уровне JVM. Они функционируют как легковесные потоки, и JVM самостоятельно управляет их планированием. Это делает Loom ближе к традиционной модели блокирующего выполнения, но без затрат на создание полноценного потока для каждой задачи.
Kotlin Coroutines:
работают на уровне библиотеки и используют систему приостановки и возобновления для создания асинхронного кода. Управление корутинами происходит за счёт компиляции, где точки приостановки обозначаются при помощи suspend.
Поддержка и интеграция:
Project Loom:
это нативное решение JVM, что означает, что любая библиотека или код, использующий блокирующие вызовы, будет поддерживать виртуальные потоки без изменений.
Kotlin Coroutines:
работают исключительно в Kotlin и требуют написания асинхронного кода с использованием suspend функций и других корутинных конструкций.
Семантика и блокировка:
Project Loom:
можно использовать «блокирующий» код с виртуальными потоками без существенных затрат на производительность.
Kotlin Coroutines:
код должен быть написан с учётом асинхронности, используя suspend и await, что требует иногда перестройки привычных синхронных алгоритмов.
Производительность:
Project Loom:
имеет преимущество на JVM, так как создаёт и управляет виртуальными потоками напрямую, что минимизирует накладные расходы по сравнению с потоками ОС.
Kotlin Coroutines:
очень производительны, так как требуют значительно меньше ресурсов для управления асинхронностью и переходами между задачами, но они не могут работать на уровне JVM и нуждаются в поддержке Kotlin.
Примеры и сравнение с Kotlin Coroutines
Простой пример запуска нескольких задач параллельно:
Project Loom:
import java.util.concurrent.Executors; public class Main { public static void main(String[] args) { try ( var executor = Executors.newVirtualThreadPerTaskExecutor() ) { executor.submit(() -> { // Ожидаемый вывод: Task 1 on VirtualThread-1 System.out.println("Task 1 on " + Thread.currentThread().getName()); }); executor.submit(() -> { // Ожидаемый вывод: Task 2 on VirtualThread-2 System.out.println("Task 2 on " + Thread.currentThread().getName()); }); // Ожидаемый вывод: Main task on main System.out.println("Main task on " + Thread.currentThread().getName()); } } }
Kotlin Coroutines:
import kotlinx.coroutines.* fun main() = runBlocking { launch { // Ожидаемый вывод: Task 1 on DefaultDispatcher-worker-1 (название потока может измениться) println("Task 1 on ${Thread.currentThread().name}") } launch { // Ожидаемый вывод: Task 2 on DefaultDispatcher-worker-2 (название потока может измениться) println("Task 2 on ${Thread.currentThread().name}") } // Ожидаемый вывод: Main task on main println("Main task on ${Thread.currentThread().name}") }
Асинхронная обработка данных с ожиданием результатов:
Project Loom:
import java.util.concurrent.Executors; public class Main { public static void main(String[] args) throws Exception { try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { var result1 = executor.submit(() -> getData1()); var result2 = executor.submit(() -> getData2()); // Ожидаемый вывод: Result: 30 System.out.println("Result: " + (result1.get() + result2.get())); } } public static int getData1() throws InterruptedException { Thread.sleep(1000); return 10; } public static int getData2() throws InterruptedException { Thread.sleep(1000); return 20; } }
Kotlin Coroutines:
import kotlinx.coroutines.* fun main() = runBlocking { val result1 = async { getData1() } val result2 = async { getData2() } // Ожидаемый вывод: Result: 30 println("Result: ${result1.await() + result2.await()}") } suspend fun getData1(): Int { delay(1000) return 10 } suspend fun getData2(): Int { delay(1000) return 20 }
Использование delay в Kotlin и аналог в Java с виртуальными потоками:
Project Loom:
public class Main { public static void main(String[] args) throws InterruptedException { var thread = Thread.startVirtualThread(() -> { // Ожидаемый вывод: Start System.out.println("Start"); try { Thread.sleep(1000); // Работает как delay } catch (InterruptedException e) { e.printStackTrace(); } // Ожидаемый вывод: End after 1 second System.out.println("End after 1 second"); }); thread.join(); } }
Kotlin Coroutines:
import kotlinx.coroutines.* fun main() = runBlocking { // Ожидаемый вывод: Start println("Start") delay(1000) // Ожидаемый вывод: End after 1 second println("End after 1 second") }
Параллельное выполнение задач с ожиданием результата:
Project Loom:
import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.Executors; public class Main { public static void main(String[] args) throws Exception { try ( var executor = Executors.newVirtualThreadPerTaskExecutor() ) { var tasks = List.of( (Callable<Integer>) () -> { return getData(1); }, (Callable<Integer>) () -> { return getData(2); }, (Callable<Integer>) () -> { return getData(3); } ); var results = executor.invokeAll(tasks) .stream() .map(future -> { try { return future.get(); } catch (Exception e) { throw new RuntimeException(e); } }) .toList(); // Ожидаемый вывод: Results: [10, 20, 30] System.out.println("Results: " + results); } } public static int getData(int id) throws InterruptedException { Thread.sleep(500); return id * 10; } }
Kotlin Coroutines:
import kotlinx.coroutines.* fun main() = runBlocking { val results = listOf( async { getData(1) }, async { getData(2) }, async { getData(3) } ).awaitAll() // Ожидаемый вывод: Results: [10, 20, 30] println("Results: $results") } suspend fun getData(id: Int): Int { delay(500) return id * 10 }
Использование каналов для обмена данными:
Project Loom:
import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; public class Main { public static void main(String[] args) throws InterruptedException { var queue = new LinkedBlockingQueue<Integer>(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> { for (int i = 1; i <= 5; i++) { queue.put(i * i); } // Специальное значение для завершения queue.put(-1); }); int value; while ((value = queue.take()) != -1) { // Ожидаемый вывод: 1, 4, 9, 16, 25 (каждое число с новой строки) System.out.println(value); } } } }
Kotlin Coroutines:
import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel fun main() = runBlocking { val channel = Channel<Int>() launch { for (x in 1..5) channel.send(x * x) channel.close() } for (y in channel) // Ожидаемый вывод: 1, 4, 9, 16, 25 (каждое число с новой строки) println(y) }
Ради разнообразия — несколько примеров на Kotlin с использованием Project Loom, чтобы показать, что так также можно
Выполнение нескольких HTTP-запросов:
Предположим, у вас есть несколько API, к которым нужно обратиться параллельно. Вот как можно сделать это с помощью виртуальных потоков.
import java.net.HttpURLConnection import java.net.URL import java.util.concurrent.Executors fun main() { val urls = listOf( "https://jsonplaceholder.typicode.com/posts/1", "https://jsonplaceholder.typicode.com/posts/2", "https://jsonplaceholder.typicode.com/posts/3" ) val executor = Executors.newVirtualThreadPerTaskExecutor() try { val tasks = urls.map { url -> executor.submit { fetchUrl(url) } } // Ожидаем завершения всех задач tasks.forEach { it.get() } } finally { executor.close() } } fun fetchUrl(urlString: String) { val url = URL(urlString) val connection = url.openConnection() as HttpURLConnection connection.requestMethod = "GET" val responseCode = connection.responseCode println("Response Code for $urlString: $responseCode") connection.inputStream.use { inputStream -> val response = inputStream.bufferedReader().readText() // Печатаем первые 100 символов println("Response Body for $urlString: ${response.take(100)}") } }
Параллельное выполнение вычислений:
В этом примере мы будем выполнять несколько вычислений параллельно.
import java.util.concurrent.Executors fun main() { val executor = Executors.newVirtualThreadPerTaskExecutor() try { val tasks = List(5) { i -> executor.submit { val result = heavyComputation(i) println("Result of computation $i: $result on thread: ${Thread.currentThread().name}") } } // Ожидаем завершения всех задач tasks.forEach { it.get() } } finally { executor.close() } } fun heavyComputation(n: Int): Int { // Эмуляция тяжелых вычислений Thread.sleep(1000) return n * n }
Работа с потоками ввода-вывода:
Этот пример демонстрирует, как виртуальные потоки могут помочь упростить работу с потоками ввода-вывода.
import java.io.File import java.util.concurrent.Executors fun main() { val fileNames = listOf("file1.txt", "file2.txt", "file3.txt") val executor = Executors.newVirtualThreadPerTaskExecutor() try { val tasks = fileNames.map { fileName -> executor.submit { readFile(fileName) } } // Ожидаем завершения всех задач tasks.forEach { it.get() } } finally { executor.close() } } fun readFile(fileName: String) { try { val content = File(fileName).readText() // Печатаем первые 50 символов println("Content of $fileName: ${content.take(50)}") } catch (e: Exception) { println("Failed to read $fileName: ${e.message}") } }
Масштабируемый веб-сервер на виртуальных потоках:
Здесь мы создаем простой HTTP-сервер, который использует виртуальные потоки для обработки каждого запроса. Это позволяет легко масштабироваться и обрабатывать большое количество запросов одновременно.
import java.net.ServerSocket import java.util.concurrent.Executors fun main() { val port = 8080 val serverSocket = ServerSocket(port) println("Server started on port $port") val executor = Executors.newVirtualThreadPerTaskExecutor() try { while (true) { val clientSocket = serverSocket.accept() executor.submit { clientSocket.use { val request = it.getInputStream().bufferedReader().readLine() println("Received request: $request on thread: ${Thread.currentThread().name}") val response = """ HTTP/1.1 200 OK Content-Type: text/plain Hello from virtual threads! """.trimIndent() it.getOutputStream().apply { write(response.toByteArray()) flush() } println("Response sent on thread: ${Thread.currentThread().name}") } } } } finally { executor.close() } }
Интеграция Project Loom с Spring
Для интеграции Project Loom с Spring можно использовать виртуальные потоки для обработки запросов, что делает асинхронное программирование более простым и эффективным, чем традиционные потоки.
Конфигурация для Tomcat:
Мы можем настроить Tomcat для использования виртуальных потоков, указав собственную фабрику потоков.
Настройка TomcatServletWebServerFactory с виртуальным пулом потоков позволяет обрабатывать каждый запрос в виртуальном потоке, экономя ресурсы при высоконагруженной работе.
import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardThreadExecutor; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.runApplication; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.annotation.Bean; @SpringBootApplication public class TomcatLoomApplication { @Bean public ServletWebServerFactory servletContainer() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.addConnectorCustomizers(connector -> { AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler(); StandardThreadExecutor executor = new StandardThreadExecutor(); executor.setNamePrefix("loom-thread-"); executor.setMaxThreads(Integer.MAX_VALUE); executor.setMinSpareThreads(10); executor.setThreadPriority(Thread.NORM_PRIORITY); // Устанавливаем фабрику виртуальных потоков executor.setThreadFactory(Thread.ofVirtual().factory()); protocol.setExecutor(executor); }); return factory; } public static void main(String[] args) { runApplication(TomcatLoomApplication.class, args); } }
Конфигурация для Jetty:
Для настройки виртуальных потоков в Jetty мы создаем ThreadPool, использующий виртуальные потоки.
Использование QueuedThreadPool с виртуальной фабрикой потоков, позволяет Jetty масштабироваться и создавать потоки по мере необходимости без затрат на системные потоки.
import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.runApplication; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.annotation.Bean; @SpringBootApplication public class JettyLoomApplication { @Bean public ServletWebServerFactory servletContainer() { QueuedThreadPool threadPool = new QueuedThreadPool( // Максимальное число виртуальных потоков, минимум потоков Integer.MAX_VALUE, 10, 60000 ); threadPool.setName("loom-thread-"); // Устанавливаем фабрику виртуальных потоков threadPool.setThreadFactory(Thread.ofVirtual().factory()); JettyServletWebServerFactory factory = new JettyServletWebServerFactory(); factory.setThreadPool(threadPool); return factory; } public static void main(String[] args) { runApplication(JettyLoomApplication.class, args); } }
Конфигурация для Undertow:
Для работы с виртуальными потоками в Undertow мы используем кастомный Worker с виртуальными потоками.
В Undertow создается XnioWorker, настроенный для использования виртуальных потоков. Это подходит для асинхронной обработки запросов и неблокирующей архитектуры Undertow.
import io.undertow.UndertowOptions; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.runApplication; import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.annotation.Bean; import org.xnio.OptionMap; import org.xnio.Options; import org.xnio.Xnio; import org.xnio.XnioWorker; @SpringBootApplication public class UndertowLoomApplication { @Bean public ServletWebServerFactory servletContainer() { XnioWorker worker = createVirtualThreadWorker(); UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory(); factory.addBuilderCustomizers(builder -> { builder.setWorker(worker); builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true); }); return factory; } private XnioWorker createVirtualThreadWorker() { Xnio xnio = Xnio.getInstance(); return xnio.createWorker(OptionMap.builder() .set(Options.THREAD_DAEMON, true) .set(Options.WORKER_IO_THREADS, 4) .set(Options.WORKER_TASK_CORE_THREADS, 10) // Неограниченное число виртуальных потоков .set(Options.WORKER_TASK_MAX_THREADS, Integer.MAX_VALUE) .set(Options.THREAD_FACTORY, Thread.ofVirtual().factory()) .getMap() ); } public static void main(String[] args) { runApplication(UndertowLoomApplication.class, args); } }
Настройка встроенного сервера Spring Boot для использования виртуальных потоков (Project Loom) на текущий момент не может быть полностью выполнена с помощью только application.properties или application.yml. Причина в том, что для использования виртуальных потоков требуется настройка кастомного ThreadFactory, а также изменение структуры пула потоков, что пока не поддерживается встроенными свойствами Spring Boot.
Тем не менее, существуют альтернативные способы настройки виртуальных потоков без необходимости явно писать код настройки в @Bean. Например, можно создать собственную @Configuration для более централизованного управления, и, по мере развития Spring Boot и серверов приложений, возможна появление более упрощенного способа.
Подход с кастомной конфигурацией на основе свойства:
Можно создать кастомную конфигурацию, чтобы при включении определенного свойства в application.properties, Spring автоматически применял настройки виртуальных потоков.
Добавим кастомное свойство в application.properties:
server.use-virtual-threads=true
Создадим конфигурационный класс, который проверяет значение свойства и применяет настройки для выбранного сервера (например, Tomcat).
В данном примере настройка server.use-virtual-threads=true активирует конфигурацию с виртуальными потоками, если это свойство установлено в application.properties. Это позволяет переключать использование виртуальных потоков через конфигурацию, не изменяя код приложения.
import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardThreadExecutor; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class VirtualThreadConfig { @Value("${server.use-virtual-threads:false}") private boolean useVirtualThreads; @Bean @ConditionalOnProperty(name = "server.use-virtual-threads", havingValue = "true") public ServletWebServerFactory servletContainer() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.addConnectorCustomizers(connector -> { AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler(); StandardThreadExecutor executor = new StandardThreadExecutor(); executor.setNamePrefix("loom-thread-"); executor.setMaxThreads(Integer.MAX_VALUE); executor.setMinSpareThreads(10); executor.setThreadPriority(Thread.NORM_PRIORITY); // Устанавливаем фабрику виртуальных потоков executor.setThreadFactory(Thread.ofVirtual().factory()); protocol.setExecutor(executor); }); return factory; } }
Использование CommandLineRunner для дополнительной гибкости:
Другой способ — создать CommandLineRunner или ApplicationRunner, который проверяет свойства и применяет виртуальные потоки, если это включено. Этот способ позволяет гибко настраивать сервер в рантайме.
Для активации этого подхода можно установить свойство -Dserver.use-virtual-threads=true при запуске Spring Boot приложения.
import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardThreadExecutor; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.stereotype.Component; @Component public class VirtualThreadCommandLineRunner implements CommandLineRunner { private final TomcatServletWebServerFactory factory; public VirtualThreadCommandLineRunner(TomcatServletWebServerFactory factory) { this.factory = factory; } @Override public void run(String... args) { boolean useVirtualThreads = Boolean.parseBoolean(System.getProperty("server.use-virtual-threads", "false")); if (useVirtualThreads) { factory.addConnectorCustomizers(connector -> { AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler(); StandardThreadExecutor executor = new StandardThreadExecutor(); executor.setNamePrefix("loom-thread-"); executor.setMaxThreads(Integer.MAX_VALUE); executor.setMinSpareThreads(10); executor.setThreadPriority(Thread.NORM_PRIORITY); // Устанавливаем фабрику виртуальных потоков executor.setThreadFactory(Thread.ofVirtual().factory()); protocol.setExecutor(executor); }); } } }
Продолжение следует (планирую добавить еще примеры и сравнение производительности)