Въведение в Project Loom
Project Loom е проект, разработван от Oracle за Java, който добавя в езика леки нишки, или „виртуални нишки“. Целта на Project Loom е да опрости писането на паралелен код, като позволи асинхронно изпълнение без явно използване на сложни абстракции като нишки, колбеци или корутини.
Основни особености
Виртуални нишки:
основното нововъведение на Loom. Виртуалните нишки са леки нишки, реализирани на ниво JVM, които позволяват да се обработват хиляди или дори милиони задачи паралелно.
Поддръжка на паралелни извиквания:
благодарение на виртуалните нишки може да се пише блокиращ код (например синхронни извиквания на sleep, wait или I/O операции), който въпреки това ще изпълнява задачите паралелно.
Поддръжка от JDK 19:
виртуалните нишки се появиха като preview функция в JDK 19, а в JDK 21 станаха стабилна (финална) функция.
Предимства
Опростен асинхронен код:
Кодът, написан с виртуални нишки, може да бъде линеен и блокиращ, не изисква асинхронни 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 нататък. Виртуалните нишки се появиха като preview функция в JDK 19 и 20, а в JDK 21 станаха стабилна функция. Проектът 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 и 20:
Loom е достъпен за използване от тази версия нататък, но като preview, което изисква изрично включване на флаг за активиране на виртуалните нишки.
JDK 21 и по-нова:
начиная с тази версия виртуалните нишки са стабилна функция и флагът за включване на preview не е нужен.
JVM на OpenJDK:
Project Loom се разработва в рамките на OpenJDK, така че всички реализации на JVM, основани на OpenJDK (например Zulu, AdoptOpenJDK и други), също поддържат виртуални нишки, ако използват JDK 19+.
JVM, които не са на OpenJDK:
Ако 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:
са много производителни, тъй като изискват значително по-малко ресурси за управление на асинхронността и преходите между задачите, но са реализирани на ниво компилатор и библиотека на Kotlin, а не на 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 (името на виртуалната нишка по подразбиране е празно) System.out.println("Task 1 on " + Thread.currentThread().getName()); }); executor.submit(() -> { // Очакван изход: Task 2 on (името на виртуалната нишка по подразбиране е празно) 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.HttpURLConnectionimport java.net.URLimport 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.Fileimport 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.ServerSocketimport 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 да използва виртуални нишки, като зададем собствен изпълнител (Executor) върху виртуални нишки.
Настройката на TomcatServletWebServerFactory с този изпълнител позволява всяка заявка да се обработва във виртуална нишка, като се спестяват ресурси при високо натоварване.
import org.apache.catalina.connector.Connector;import java.util.concurrent.Executors;import org.apache.coyote.http11.AbstractHttp11Protocol;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.SpringApplication;import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;import org.springframework.boot.web.servlet.server.ServletWebServerFactory;import org.springframework.context.annotation.Bean;
@SpringBootApplicationpublic class TomcatLoomApplication {
@Bean public ServletWebServerFactory servletContainer() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.addConnectorCustomizers(connector -> { AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
// Всяка заявка се обработва в нова виртуална нишка protocol.setExecutor(Executors.newThreadPerTaskExecutor( Thread.ofVirtual().name("loom-thread-", 0).factory())); }); return factory; }
public static void main(String[] args) { SpringApplication.run(TomcatLoomApplication.class, args); }
}Конфигурация за Jetty:
За настройване на виртуалните нишки в Jetty настройваме ThreadPool така, че заявките да се изпълняват във виртуални нишки.
Използването на QueuedThreadPool с изпълнител на виртуални нишки позволява на Jetty да се мащабира и да създава нишки при необходимост, без разходите за системни нишки.
import java.util.concurrent.Executors;import org.eclipse.jetty.util.thread.QueuedThreadPool;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.SpringApplication;import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;import org.springframework.boot.web.servlet.server.ServletWebServerFactory;import org.springframework.context.annotation.Bean;
@SpringBootApplicationpublic class JettyLoomApplication {
@Bean public ServletWebServerFactory servletContainer() { JettyServletWebServerFactory factory = new JettyServletWebServerFactory(); factory.addServerCustomizers(server -> { QueuedThreadPool threadPool = (QueuedThreadPool) server.getThreadPool(); threadPool.setName("loom-thread-"); // Заявките се изпълняват във виртуални нишки threadPool.setVirtualThreadsExecutor(Executors.newThreadPerTaskExecutor( Thread.ofVirtual().name("loom-thread-", 0).factory())); }); return factory; }
public static void main(String[] args) { SpringApplication.run(JettyLoomApplication.class, args); }
}Конфигурация за Undertow:
За работа с виртуални нишки в Undertow задаваме за сървлетите изпълнител върху виртуални нишки.
В Undertow се създава XnioWorker за вход-изход, а сървлетите се изпълняват от изпълнител върху виртуални нишки. Това е подходящо за асинхронна обработка на заявки и за неблокиращата архитектура на Undertow.
import io.undertow.UndertowOptions;import java.io.IOException;import java.util.concurrent.Executors;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.SpringApplication;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;
@SpringBootApplicationpublic class UndertowLoomApplication {
@Bean public ServletWebServerFactory servletContainer() throws IOException { XnioWorker worker = createWorker(); UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory(); factory.addBuilderCustomizers(builder -> { builder.setWorker(worker); builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true); }); // Сървлетите се изпълняват във виртуални нишки factory.addDeploymentInfoCustomizers(deploymentInfo -> deploymentInfo.setExecutor( Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("loom-thread-", 0).factory()))); return factory; }
private XnioWorker createWorker() throws IOException { Xnio xnio = Xnio.getInstance(); return xnio.createWorker(OptionMap.builder() .set(Options.THREAD_DAEMON, false) .set(Options.WORKER_IO_THREADS, 4) .set(Options.WORKER_TASK_CORE_THREADS, 10) .getMap() ); }
public static void main(String[] args) { SpringApplication.run(UndertowLoomApplication.class, args); }
}Започвайки от Spring Boot 3.2 и JDK 21, виртуалните нишки могат да се включат с едно вградено свойство — spring.threads.virtual.enabled=true в application.properties или application.yml. В по-ранните версии на Spring Boot настройката на собствена ThreadFactory и промяната в структурата на пула от нишки не се поддържаха от вградените свойства.
Въпреки това съществуват алтернативни начини за настройване на виртуалните нишки, без да е нужно изрично да се пише код за настройка в @Bean. Например може да се създаде собствена @Configuration за по-централизирано управление.
Подход със собствена конфигурация на базата на свойство:
Може да се създаде собствена конфигурация, така че при включване на определено свойство в 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 java.util.concurrent.Executors;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;
@Configurationpublic 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();
// Всяка заявка се обработва в нова виртуална нишка protocol.setExecutor(Executors.newThreadPerTaskExecutor( Thread.ofVirtual().name("loom-thread-", 0).factory())); }); return factory; }
}Използване на WebServerFactoryCustomizer за допълнителна гъвкавост:
Друг начин е да се създаде WebServerFactoryCustomizer, който проверява свойствата и прилага виртуалните нишки, ако това е включено. Този начин позволява гъвкаво да се настройва сървърът, без да се създава собствена фабрика. Настройката трябва да се извърши преди стартирането на сървъра, затова CommandLineRunner и ApplicationRunner тук не вършат работа: те се извикват, когато сървърът вече е стартиран, и промените във фабриката не оказват никакво влияние.
За активиране на този подход може да се зададе свойството -Dserver.use-virtual-threads=true при стартиране на Spring Boot приложението.
import java.util.concurrent.Executors;import org.apache.catalina.connector.Connector;import org.apache.coyote.http11.AbstractHttp11Protocol;import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;import org.springframework.boot.web.server.WebServerFactoryCustomizer;import org.springframework.stereotype.Component;
@Componentpublic class VirtualThreadCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override public void customize(TomcatServletWebServerFactory factory) { boolean useVirtualThreads = Boolean.parseBoolean(System.getProperty("server.use-virtual-threads", "false")); if (useVirtualThreads) { factory.addConnectorCustomizers(connector -> { AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
// Всяка заявка се обработва в нова виртуална нишка protocol.setExecutor(Executors.newThreadPerTaskExecutor( Thread.ofVirtual().name("loom-thread-", 0).factory())); }); } }
}Следва продължение (планирам да добавя още примери и сравнение на производителността)