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

推荐订阅源

Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
量子位
腾讯CDC
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
The Hacker News
The Hacker News
T
The Exploit Database - CXSecurity.com
B
Blog
S
SegmentFault 最新的问题
P
Privacy & Cybersecurity Law Blog
T
Threatpost
博客园 - 聂微东
T
Tailwind CSS Blog
The Last Watchdog
The Last Watchdog
C
Check Point Blog
N
Netflix TechBlog - Medium
D
DataBreaches.Net
爱范儿
爱范儿
IT之家
IT之家
S
Secure Thoughts
M
MIT News - Artificial intelligence
NISL@THU
NISL@THU
C
Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
有赞技术团队
有赞技术团队
A
Arctic Wolf
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
Schneier on Security
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
雷峰网
雷峰网
Project Zero
Project Zero
博客园 - Franky
H
Heimdal Security Blog
A
About on SuperTechFans
Security Latest
Security Latest
Webroot Blog
Webroot Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hugging Face - Blog
Hugging Face - Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Project Loom: Virtual Threads, Scoped Values и preview #7 Structured Concurrency
rurikovich · 2026-05-22 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение9 мин

Охват и читатели74

Обзор

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

Недавно я написал серию статей про модели конкурентности в разных JVM-языках. И неожиданно именно первая статья, про Java, получила самый заметный положительный отклик у читателей Хабра. Поэтому я решил сделать отдельный подробный обзор актуального на май 2026 года состояния Project Loom.

В Project Loom есть virtual threads, которые стали финальной фичей еще в Java 21. Есть Scoped Values, которые дошли до final в Java 25. И есть Structured Concurrency, которая все еще остается preview API и продолжает меняться.

В этой части будет теория: что вообще меняет Loom в модели Java-конкурентности, зачем нужны virtual threads, почему рядом появились Scoped Values и какую дыру пытается закрыть Structured Concurrency. Код будет, но короткий, скорее для иллюстрации идеи.

Во второй части будет практика: как включить preview Structured Concurrency, какие флаги нужны для javac, Maven и Gradle, и как выглядят рабочие примеры со ScopedValue и StructuredTaskScope.

Погнали.


1. Что такое Project Loom

Project Loom - это большой проект OpenJDK про конкурентность. Его самая известная часть: virtual threads.

Но Loom не только про “миллион потоков”. Основная идея проще: серверный код на Java снова можно писать обычным последовательным способом:

User user = userClient.load(userId);
List<Order> orders = orderClient.loadOrders(userId);
return render(user, orders);

А не обязательно превращать обычную бизнес-операцию в цепочку callback’ов, CompletableFuture, reactive streams или в локальный филиал теории категорий.

Reactive-подход нужен не везде. Часто backend-код делает обычную работу: сходить в базу, вызвать пару внутренних сервисов, собрать DTO и вернуть ответ. Для такого сценария последовательный код обычно читается проще.

Историческая проблема Java была в том, что обычный Thread дорогой. Он связан с потоком операционной системы, стеком, планировщиком ОС и прочими не бесплатными вещами. Если на каждый запрос, каждое ожидание БД и каждый HTTP-вызов заводить platform thread, то масштабироваться можно, но до определенного момента.

Поэтому мы привыкли к пулам:

ExecutorService executor = Executors.newFixedThreadPool(32);

Пулы были хорошим инженерным компромиссом эпохи дорогих потоков. Но за компромисс приходится платить:

  • нужно выбирать размер пула;

  • нужно думать про очередь;

  • blocking I/O может съесть все worker threads;

  • один общий пул внезапно становится общей точкой боли;

  • Future легко уезжает жить своей жизнью.

Раньше поток был ресурсом, который надо беречь. После Loom поток снова становится удобной единицей моделирования задачи. Не всегда, не везде, но для типичного I/O-heavy backend - очень часто.

На момент написания актуальный GA-релиз - Java 26. Текущий LTS-релиз - Java 25. JDK 27 уже доступен как early access и несет очередную preview-итерацию Structured Concurrency.

Если смотреть именно на Loom, состояние такое:

  • virtual threads стали финальной фичей в Java 21;

  • Scoped Values стали финальной фичей в Java 25;

  • Structured Concurrency все еще остается preview API.

Эта хронология важна. Loom часто обсуждают как один большой пакет, но разные его части находятся в разном состоянии зрелости. Virtual threads и Scoped Values уже можно рассматривать как нормальные инструменты платформы. Structured Concurrency пока лучше держать в голове как близкое будущее, которое уже можно щупать руками, но осторожно тащить в production.


2. Virtual Threads: дешевые потоки без новой модели мышления

Virtual thread - это поток Java, которым управляет JVM, а не напрямую операционная система. Под капотом много virtual threads выполняются на меньшем количестве carrier threads, то есть обычных platform threads.

Грубая схема:

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

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

Для прикладного Java-разработчика код остается обычным:

Thread.startVirtualThread(() -> {
    User user = userClient.load(userId);
    List<Order> orders = orderClient.loadOrders(userId);
    render(user, orders);
});

Или через executor:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<User> user = executor.submit(() -> userClient.load(userId));
    Future<List<Order>> orders = executor.submit(() -> orderClient.loadOrders(userId));

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

newVirtualThreadPerTaskExecutor() звучит как ересь, если много лет жить с правилом “потоки надо переиспользовать”. Но для virtual threads это нормальная модель: один virtual thread на задачу.

Вот тут и меняется инженерное ощущение. Раньше поток был дорогим объектом инфраструктуры. Его надо было экономить, переиспользовать, прятать в пул и надеяться, что размер пула выбран не слишком плохо. После Loom поток снова становится почти буквальным отражением задачи.

Почти.

Virtual threads не ускоряют CPU-bound код. Если вы считаете хэши, сжимаете видео или гоняете тяжелую математику, то миллион virtual threads не создаст миллион ядер. Скорее наоборот, можно сделать хуже.

Virtual threads не отменяют backpressure. Если база выдерживает 50 одновременных запросов, то запуск 5000 virtual threads просто упрется в тот же лимит, только быстрее. Раньше thread pool случайно был еще и ограничителем. Теперь ограничитель часто надо делать явно: semaphore, rate limiter, connection pool, bulkhead.

Старые инженерные вопросы никуда не делись, просто переехали в более честное место.


3. Что virtual threads не решают

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

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

Допустим, у нас есть endpoint, который собирает страницу пользователя:

  • профиль из одного сервиса;

  • заказы из второго;

  • рекомендации из третьего.

Классический код на ExecutorService легко написать так:

Future<User> user = executor.submit(() -> userClient.load(userId));
Future<List<Order>> orders = executor.submit(() -> orderClient.loadOrders(userId));
Future<List<Recommendation>> recommendations =
        executor.submit(() -> recommendationClient.load(userId));

return new UserPage(user.get(), orders.get(), recommendations.get());

В учебном примере все красиво. В жизни сразу появляются вопросы.

Что будет, если userClient.load() упал быстро, а recommendationClient.load() завис на 5 секунд? Кто отменит остальные задачи? Что увидит thread dump? Что будет, если родительский HTTP-запрос уже отменен клиентом? А если мы забыли вызвать cancel()? А если get() вызвали только для первой future, получили exception и вышли из метода?

Получаются фоновые хвосты. Задача логически принадлежала одному запросу, но технически продолжает жить отдельно.

Это классическая болезнь неструктурированной конкурентности: родительская операция умерла, а ее дети еще бегают где-то по runtime. Потом ты смотришь на графики внешнего API и думаешь: “А кто вообще сейчас туда ходит?”

Именно здесь появляется Structured Concurrency. Но перед ней надо разобрать еще одну часть Loom: как передавать контекст.


4. Scoped Values: контекст с ограниченной областью действия

В backend-коде контекст есть почти всегда:

  • request id;

  • tenant id;

  • текущий пользователь;

  • trace/span;

  • security context;

  • настройки локали.

Классический вариант в Java: ThreadLocal.

private static final ThreadLocal<RequestContext> REQUEST_CONTEXT =
        new ThreadLocal<>();

Работало? Да. И сейчас работает.

Но у ThreadLocal есть неприятный характер. Значение привязано к потоку, а не к логической операции. Если поток переиспользуется в пуле, забытый remove() превращается в классический источник странностей. В тестах все зеленое, а потом в проде один запрос внезапно видит кусочек контекста другого запроса. Не каждый день, конечно. По праздникам.

С virtual threads ситуация меняется. Потоки становятся дешевыми, их может быть очень много, и использовать ThreadLocal как универсальный контейнер для контекста становится менее приятно. Особенно если в него кладут тяжелые объекты, mutable-состояние или целые графы зависимостей.

ScopedValue предлагает другую модель: значение не “приклеивается к потоку”, а привязывается к ограниченной области выполнения.

public static final ScopedValue<RequestContext> REQUEST =
        ScopedValue.newInstance();

Использование выглядит так:

return ScopedValue
        .where(REQUEST, context)
        .call(() -> service.handle(request));

Внутри service.handle(request) и глубже по стеку можно получить текущий контекст:

RequestContext context = REQUEST.get();

После выхода из call() binding исчезает. Не надо делать remove(). Нельзя случайно переприсвоить значение где-то в середине бизнес-операции. Если REQUEST.get() вызвать вне области, где значение связано, будет ошибка. И это хорошо: баг падает рядом с причиной, а не размазывается по логам.

Есть важное ограничение: scoped value лучше использовать для небольших неизменяемых данных. Не надо класть туда mutable map “на все случаи жизни” или сервис-локатор. Иначе получится тот же глобальный контекст, только в новой упаковке.

Хороший кандидат:

public record RequestContext(
        String requestId,
        String tenantId,
        String userId
) {
}

Плохой кандидат:

public final class RequestBag {
    public final Map<String, Object> everything = new HashMap<>();
}

Второй вариант рано или поздно превращается в помойку. Я такие видел. Сначала туда кладут request id. Потом текущего пользователя. Потом “временно” feature flags. Потом кто-то добавляет EntityManager, потому что очень надо. Через год такой объект уже сложно назвать контекстом: это общий контейнер для всего подряд.


5. Structured Concurrency: управление группой связанных задач

Structured Concurrency вводит простое правило:

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

Не глобальный submit в никуда. А scope, внутри которого задачи стартуют, завершаются, отменяются и наблюдаются как единое целое.

В Java эта идея строится вокруг StructuredTaskScope.

Короткая форма:

try (var scope = StructuredTaskScope.open()) {
    var user = scope.fork(() -> userClient.load(userId));
    var orders = scope.fork(() -> orderClient.loadOrders(userId));

    scope.join();

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

У scope есть владелец: поток, который открыл scope. Только он управляет жизненным циклом. Дочерние задачи запускаются через fork(). Потом владелец вызывает join() и ждет результат по выбранной политике.

Когда scope закрывается, структура не должна оставлять мусор после себя. Если задача не завершилась штатно, scope может ее отменить. Если одна задача упала, политика join’а может отменить остальные. Если сработал timeout, scope опять же знает, кого надо остановить.

Это почти как try-with-resources, только ресурсом становится группа связанных задач.

Вот это мне и нравится в концепции. Она не пытается сделать конкурентность “невидимой”. В коде появляется явная граница: тут создали дочерние задачи, тут дождались результата, тут они больше не должны жить.


6. Joiner: политика ожидания становится явной

В старом коде политика ожидания часто размазана по коду. Где-то get(), где-то cancel(), где-то anyOf(), где-то exceptionally(), где-то забыли обработать один из сценариев.

В текущей preview-линии Structured Concurrency эта политика выносится в Joiner.

Например, “успешны должны быть все”:

try (var scope = StructuredTaskScope.open(
        Joiner.<String>allSuccessfulOrThrow())) {

    scope.fork(() -> loadPart("header"));
    scope.fork(() -> loadPart("body"));
    scope.fork(() -> loadPart("footer"));

    return scope.join();
}

Или “достаточно первого успешного ответа”:

try (var scope = StructuredTaskScope.open(
        Joiner.<Price>anySuccessfulOrThrow())) {

    scope.fork(() -> priceClientA.load(sku));
    scope.fork(() -> priceClientB.load(sku));
    scope.fork(() -> priceClientC.load(sku));

    return scope.join();
}

Это не про красоту синтаксиса. Это про то, что жизненный цикл группы задач становится частью конструкции, а не набором договоренностей между программистами.


7. Почему Structured Concurrency все еще preview

Virtual threads прошли путь:

  • JEP 425: preview в JDK 19;

  • JEP 436: second preview в JDK 20;

  • JEP 444: final в JDK 21.

Structured Concurrency идет дольше:

  • JEP 428: incubator в JDK 19;

  • JEP 437: second incubator в JDK 20;

  • JEP 453: preview в JDK 21;

  • JEP 462: second preview в JDK 22;

  • JEP 480: third preview в JDK 23;

  • JEP 499: fourth preview в JDK 24;

  • JEP 505: fifth preview в JDK 25;

  • JEP 525: sixth preview в JDK 26;

  • JEP 533: seventh preview для JDK 27.

Почему так долго?

Потому что тут сложнее, чем просто “сделать легкий поток”. Virtual threads можно встроить в существующую модель Thread. Да, внутри JVM там огромная работа, но снаружи программист видит знакомый API.

Structured Concurrency трогает дизайн программирования. Надо ответить на вопросы:

  • как выражать политику ожидания;

  • что возвращает join();

  • как передавать результаты разных типов;

  • что делать с исключениями;

  • как должны работать timeouts;

  • как это будет выглядеть в диагностике;

  • как не закрыть API слишком рано и не тащить неудобную форму следующие 20 лет.

И API реально менялся.

Ранние preview-версии были ближе к наследникам StructuredTaskScope, например ShutdownOnFailure и ShutdownOnSuccess. Потом в JDK 25 публичные конструкторы заменили на static factory methods вроде StructuredTaskScope.open(). Появилась модель Joiner, где политика ожидания и результат join() становятся явной частью открытия scope.

В JDK 26 шлифовали joiner’ы и timeout’ы. Например, allSuccessfulOrThrow() стал возвращать список результатов, а не поток subtasks. anySuccessfulResultOrThrow() переименовали в более короткий anySuccessfulOrThrow().

В JEP 533 основная правка вокруг исключений. Стандартные joiner’ы теперь идут в сторону ExecutionException, а не preview-специфичного FailedException. Еще добавили типизацию исключения, которое может бросить join(). Звучит мелко, но для Java это не мелочь: checked exceptions, миграция последовательного кода в конкурентный и совместимость с привычной моделью Future.get() очень быстро становятся практическими вопросами.

Занудно? Немного. Но лучше занудно в preview, чем больно после final.


Вывод

Project Loom уже изменил Java через virtual threads. Это финальная фича с Java 21, и она возвращает backend-разработчику право писать нормальный блокирующий код там, где он естественен.

Scoped Values закрывают соседнюю боль: передачу request context без вечного ThreadLocal и без протаскивания служебных параметров через половину приложения.

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

Virtual threads и Scoped Values уже можно брать как рабочие инструменты. Structured Concurrency - ближайшее будущее Java-конкурентности: изучать стоит уже сейчас, а внедрять с учетом preview-статуса.

В следующей части будет меньше теории и больше кода: как включить preview, как собрать примеры, как выглядит ScopedValue в сервисном коде и как использовать StructuredTaskScope, когда нужно параллельно сходить в несколько источников и собрать общий результат.


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


Легких Вам Релизов!