Всем пример и сегодня поговорим про Виртуальные потоки (Virtual Threads)



Обычный поток ОС весит около 1–2 мегабайт памяти только под стек плюс накладные расходы ядра. Сервер начинает задыхаться уже на паре сотен активных потоков из-за дорогого переключения контекста процессором. Поэтому высоконагруженные системы заставляли нас писать сложный асинхронный код со стрелочками .thenApply().

В JDK 21 появились виртуальные потоки. Это легковесные объекты внутри JVM. Их создание стоит копейки, а потребляют они минимум памяти.

Когда код делает блокирующую операцию (jdbcTemplate.query(), httpClient.send(), Thread.sleep()), виртуальный поток паркуется. Он открепляется от физического ядра процессора. Как только данные пришли или время вышло — поток просыпается, цепляется к любому свободному ядру и продолжает работу ровно с той же строчки кода.

Как пишем в коде:
Просто меняем свой старый пул потоков на новый:

/

/ Было (или обычный фиксированный пул):
ExecutorService oldPool = Executors.newFixedThreadPool(200);

// Стало:
ExecutorService newPool = Executors.newVirtualThreadPerTaskExecutor();

Весь остальной код остается обычным, синхронным и никаких реактивных подписок.

Например: Вызовем API 10 000 раз одновременно

Представим задачу: нам нужно сходить во внешнее REST-API по списку из 10 000 ID, получить JSON и сохранить его в базу. Сеть медленная, каждый запрос ждет ответа ~200 миллисекунд.

Вариант А: Старый подход
Если запустить 10 000 обычных потоков через `newFixedThreadPool`, сервер упадет. Мы были вынуждены ставить лимит, например, 50 потоков. Остальные 9 950 задач стояли в очереди. Время выполнения растягивалось до небес.


/

/ СТАРЫЙ КОД: НЕЛЬЗЯ ОТКРЫТЬ МНОГО ПОТОКОВ
try (var pool = Executors.newFixedThreadPool(50)) { // Лимит 50!
List<CompletableFuture<?>> futures = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
int id = i;
CompletableFuture<Void> f = CompletableFuture.runAsync(() -> {
String data = callApiBlocking(id); // Ждем сеть...
saveToDb(data);
}, pool);
futures.add(f);
}
// Будем ждать очень долго, пока 10к задач пролезут через узкое горлышко в 50 потоков
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}

Вариант Б: Новый подход (с Virtual Threads)

Мы просто говорим: «Запусти 10 000 задач». Код выглядит пугающе простым для такой нагрузки.

// НОВЫЙ КОД: НИКАКИХ ЛИМИТОВ, ПРОСТО ПИШЕМ ЦИКЛ
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {

// Запускаем 10 000 виртуальных потоков разом
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
// Внутри здесь происходит магия:
// Метод callApiBlocking использует SocketInputStream.read().
// Когда пакеты из сети еще не пришли, JVM "паркует" этот виртуальный поток.
// Реальное ядро CPU мгновенно освобождается для других 9 999 задач.
String response = callApiBlocking(i);

// Пока ждем ответ базы данных, поток снова спит, не занимая процессор.
saveToDb(response);
});
});
} // try-with-resources гарантирует, что main-поток дождется завершения всех 10 000 задач

Хотя запущено 10 000 потоков, физически занято будет всего несколько ядер вашего процессора (обычно столько, сколько у вас физических ядер или чуть больше). Когда первый поток уйдет в ожидание сети, JVM тут же даст его процессорное время второму, третьему или десятитысячному. Тысячи сокетов могут быть открыты одновременно, но нативных потоков ОС потребуется единицы.
Просмотры: 33
Открыть в MAX Сообщить о неточности

Репосты публикации

Публикация канала

Сообщить о неточности

Опишите, что нужно исправить. Если есть источник, добавьте ссылку.

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти