Roman Kryvolapov Engineering Blog

Вступ до Project Loom

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);
});
}
}
}

Далі буде (планую додати ще приклади та порівняння продуктивності)

Copyright: Roman Kryvolapov