惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
云风的 BLOG
云风的 BLOG
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
GbyAI
GbyAI
C
Check Point Blog
M
MIT News - Artificial intelligence
T
The Blog of Author Tim Ferriss
Jina AI
Jina AI
博客园 - 【当耐特】
U
Unit 42
月光博客
月光博客
腾讯CDC
Y
Y Combinator Blog
小众软件
小众软件
博客园_首页
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
The GitHub Blog
The GitHub Blog
博客园 - 聂微东
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
T
Tailwind CSS Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Сравнение моделей конкурентности JVM языков: Треды, Пулы ...
Растегаев Юрий · 2026-05-13 · via Все публикации подряд на Хабре

Средний

6 мин

15K

Привет, Хабр !

Многие если не все встречались с потоками, пулами потоков и проблемами многопоточности и конкурентности. В JVM языках под капотом одна и таже платформа, но Java, Kotlin, Scala и Clojure по-разному работают с потоками, задачами и ожиданием I/O.

В Java кроме классических потоков появились virtual threads. В Kotlin давно есть корутины. В Scala есть ZIO со своим runtime и fiber’ами. В Clojure есть futures, promises, agents и core.async. Снаружи это всё может выглядеть как разные способы выполнить работу параллельно, но внутри модели сильно отличаются.

Мы сравним эти подходы на простых для backend-разработки вещах: как запускается задача, где используются реальные потоки, а где логические, что происходит при ожидании I/O и кто отвечает за отмену. Заодно будет видно, почему в Java можно спокойно писать блокирующий код, а в Kotlin, Scala и Clojure чаще появляются async, await, flatMap.

В первой части начну с Java-базы: обычный Thread, thread pools, virtual threads из Project Loom и Structured Concurrency. В следующих будут корутины, zio-runtime с fibers и Clojure.


1. Классический Java Thread

Начнём с базы. В Java Thread исторически был довольно прямым отражением потока операционной системы. Не всегда один-в-один в деталях реализации JVM и ОС, но для прикладного программиста модель была примерно такая:

java.lang.Thread -> native/platform thread -> OS scheduler

У такого потока есть стек, системные структуры, участие планировщика ОС, переключение контекста. Это не бесплатная штука. Создавать поток под каждую мелкую задачу можно, но недолго.

Java: создание обычного потока
Thread thread = new Thread(() -> {
    System.out.println("Working in " + Thread.currentThread());
});

thread.start();
thread.join();

В маленькой программе всё выглядит нормально. В сервере под нагрузкой начинается веселье: тысячи соединений, ожидание БД, внешние HTTP-вызовы, очереди, таймауты. Если на каждый запрос заводить платформенный поток, можно быстро упереться не в CPU, а в память и планировщик.

Тут и появляются пулы.


2. Thread pools: компромисс эпохи дорогих потоков

Идея thread pool простая: потоки дорогие, значит, будем их переиспользовать.

Вместо того чтобы создать 10 000 потоков под 10 000 задач, создаём, например, 16 потоков для условного backend-инстанса на 16 vCPU (virtual CPU) и очередь задач перед ними.

Java: классический ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(16);

Future<User> userFuture = executor.submit(() -> userRepository.loadUser(userId));
Future<List<Order>> ordersFuture = executor.submit(() -> orderClient.loadOrders(userId));

User user = userFuture.get();
List<Order> orders = ordersFuture.get();

executor.shutdown();

Сама идея отличная. Потоки не создаются на каждый чих, перед ними появляется очередь, можно ограничить параллелизм и не пустить в базу больше запросов, чем она переварит. Ну и классика: отдельный pool для CPU-bound, отдельный для blocking I/O, плюс правило на случай, если очередь переполнится.

Но пул потоков быстро превращается в инженерную задачу, а не просто в одну строчку кода.

Сколько потоков надо? 8? 16? 200? Размер очереди ограничивать или пусть будет unbounded? Что делать, если в ForkJoinPool.commonPool() случайно попала блокирующая операция? Почему один сервис завис, хотя CPU свободен? Почему CompletableFuture не выполняется? Почему в thread dump все ждут JDBC?

Знакомо?

CPU-bound и I/O-bound

Для CPU-bound задач логика понятная: если у нас 8 ядер, то 800 потоков не сделают вычисления быстрее. Они сделают больше переключений контекста.

Для I/O-bound задач всё хитрее. Поток может почти всё время ждать: БД, сеть, файловую систему, очередь. Поэтому потоков нужно больше, чем ядер. Но насколько больше? Ответ обычно звучит как “it depends”, и это неприятно, потому что зависит правда от всего.

Пулы потоков не исчезли и после Loom. Просто у них меняется роль. Раньше пул часто был способом экономить потоки. Сейчас он всё чаще нужен как ограничитель: не пустить в БД больше 50 запросов, не положить чужой API, не превратить сервис в генератор таймаутов.


3. Зелёные потоки и virtual threads

Зелёные потоки - это потоки, которыми управляет не операционная система, а runtime языка или виртуальной машины. Проще: много лёгких логических потоков могут жить поверх меньшего числа настоящих OS threads.

В Java зелёные потоки были ещё в ранних версиях платформы, потом Java ушла в native threads. А теперь идея вернулась, но уже в современном виде: Project Loom и virtual threads.

Почему Java ушла от ранних green threads

Короткая версия: ОС научились хорошо планировать native threads, а серверной Java нужно было нормально использовать несколько процессоров и ядер.

Старые green threads были удобны для переносимости, но плохо дружили с настоящим параллелизмом, blocking system calls, native-библиотеками, профайлерами и отладчиками. Для растущих серверных приложений это стало слишком большим ограничением.

Первоисточник по Loom: JEP 444: Virtual Threads.

Virtual threads стали стабильными в Java 21.

Java: virtual thread на одну задачу
Thread.startVirtualThread(() -> {
    var user = userRepository.loadUser(userId);
    var orders = orderClient.loadOrders(userId);
    render(user, orders);
});

На вид это обычный Thread. И это не случайность. Loom специально сохраняет привычную Java-модель: обычные методы, обычные stack traces, обычные блокирующие вызовы. Только поток теперь лёгкий.

На пальцах схема такая:

много virtual threads
        |
        v
меньше carrier/platform threads
        |
        v
потоки ОС

Когда virtual thread делает blocking I/O, JVM может “отмонтировать” его от carrier thread. Carrier освобождается и выполняет другую работу. Когда I/O завершится, virtual thread продолжит выполнение.

Для программиста это выглядит почти магически: код блокирующий, но платформа старается не держать настоящий поток заблокированным там, где это можно сделать иначе.

Java: executor, который создаёт virtual thread на задачу
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<User> user = executor.submit(() -> userRepository.loadUser(userId));
    Future<List<Order>> orders = executor.submit(() -> orderClient.loadOrders(userId));

    return render(user.get(), orders.get());
}

Тут важный сдвиг в голове. Мы больше не обязаны переиспользовать virtual threads. Их нормально создавать под задачу. newVirtualThreadPerTaskExecutor() звучит почти как ересь для человека, воспитанного на newFixedThreadPool, но в этом и смысл.

Где virtual threads хороши

На практике они особенно хорошо ложатся на классический backend:

  • request-per-thread модель;

  • JDBC;

  • blocking HTTP clients;

  • RPC;

  • очереди;

  • много одновременного ожидания.

Это не ускоритель CPU. Если у вас 8 ядер и вы считаете хэши, виртуальный миллион потоков не превратит машину в суперкомпьютер.

Зато если у вас 10 000 запросов в основном ждут сеть и БД, virtual threads возвращают право писать нормальный последовательный код без пирамиды callback’ов и без обязательного reactive-стека.


4. Structured Concurrency

Virtual threads отвечают на вопрос: “как дешево запустить много задач?”

Но есть другой вопрос: “как потом не потерять эти задачи?”

Обычный ExecutorService легко рождает сиротские Future: одна задача упала, вторая продолжает работать, родительский запрос уже отменён, а где-то в фоне ещё летит HTTP-вызов. По thread dump потом можно гадать, кто кого породил.

Structured Concurrency пытается вернуть структуру туда, где раньше была куча ручных submit/get/cancel.

Стартовая JEP-ссылка: JEP 428: Structured Concurrency. Дальше API несколько раз проходил через preview-итерации.

JEP по Structured Concurrency

Virtual threads уже стабильны. А вот structured concurrency в Java ещё проходит preview-стадии, поэтому мелкие детали API пока лучше считать временными. Они уже менялись и ещё могут поменяться.

Java: идея StructuredTaskScope
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var user = scope.fork(() -> userRepository.loadUser(userId));
    var orders = scope.fork(() -> orderClient.loadOrders(userId));

    scope.join();
    scope.throwIfFailed();

    return render(user.get(), orders.get());
}

Я смотрю на это не как на новый красивый синтаксис, а как на попытку вернуть задачам нормальный жизненный цикл:

  • дочерние задачи живут внутри scope;

  • родитель ждёт их завершения;

  • при ошибке можно отменить остальные;

  • при выходе из блока не остаётся фоновых хвостов;

  • stack trace и диагностика становятся ближе к реальной структуре программы.

Рядом с Loom стоит ещё одна важная штука: JEP 506: Scoped Values. Это способ передавать контекст вниз по вызовам и дочерним задачам. Что-то из мира ThreadLocal, но лучше подходит к миллионам virtual threads и structured concurrency.


Вывод

Классические thread pools появились как ответ на дорогие platform threads. Они помогают переиспользовать потоки, ограничивать параллелизм и контролировать очередь задач, но вместе с этим добавляют отдельный слой настройки: размер пула, размер очереди, разные пулы для CPU-bound и blocking I/O.

Virtual threads меняют эту картину. Их не нужно переиспользовать как обычные потоки, поэтому Java снова позволяет писать последовательный blocking-код, но запускать много независимых задач дешевле. При этом ограничения никуда не исчезают: базу, внешний API или очередь всё равно нужно защищать лимитами.

Structured Concurrency закрывает другой вопрос: не только как запустить задачи, но и как управлять их жизненным циклом. Чтобы дочерние задачи не жили отдельно от родительского запроса, нормально отменялись и не превращались в фоновые хвосты.


Во второй части перейдём к JVM-языкам, которые решают похожие задачи уже иначе: Kotlin coroutines, ZIO runtime и Clojure.


Если вам близки темы разработки, рефакторинга, архитектуры и стартапов, буду рад видеть вас в моём Telegram-канале.


Ссылки