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

推荐订阅源

GbyAI
GbyAI
Cyberwarzone
Cyberwarzone
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
W
WeLiveSecurity
博客园 - 叶小钗
Hugging Face - Blog
Hugging Face - Blog
Security Latest
Security Latest
Scott Helme
Scott Helme
TaoSecurity Blog
TaoSecurity Blog
N
Netflix TechBlog - Medium
爱范儿
爱范儿
Application and Cybersecurity Blog
Application and Cybersecurity Blog
G
Google Developers Blog
F
Fortinet All Blogs
N
News and Events Feed by Topic
V2EX - 技术
V2EX - 技术
Google Online Security Blog
Google Online Security Blog
L
LINUX DO - 热门话题
NISL@THU
NISL@THU
The GitHub Blog
The GitHub Blog
Spread Privacy
Spread Privacy
S
Secure Thoughts
T
Tailwind CSS Blog
Google DeepMind News
Google DeepMind News
Recorded Future
Recorded Future
N
News and Events Feed by Topic
SecWiki News
SecWiki News
S
Security @ Cisco Blogs
A
About on SuperTechFans
云风的 BLOG
云风的 BLOG
L
Lohrmann on Cybersecurity
P
Palo Alto Networks Blog
Know Your Adversary
Know Your Adversary
IT之家
IT之家
人人都是产品经理
人人都是产品经理
Attack and Defense Labs
Attack and Defense Labs
Hacker News - Newest:
Hacker News - Newest: "LLM"
MyScale Blog
MyScale Blog
宝玉的分享
宝玉的分享
T
The Blog of Author Tim Ferriss
H
Hacker News: Front Page
T
Tenable Blog
C
CERT Recently Published Vulnerability Notes
D
DataBreaches.Net
阮一峰的网络日志
阮一峰的网络日志
Help Net Security
Help Net Security
博客园_首页
S
Securelist
罗磊的独立博客

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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? Примеры и код
rurikovich · 2026-05-23 · via Все публикации подряд на Хабре

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

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

Охват и читатели15K

Обзор

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

В первой части я разобрал теорию Project Loom: virtual threads, Scoped Values и preview Structured Concurrency. Там была общая картина: зачем Loom появился, почему virtual threads не ускоряют любой код, чем ScopedValue отличается от ThreadLocal, зачем нужны Joiner и почему Structured Concurrency все еще preview.

Теперь практика.

В этой части будет меньше теории и больше кода:

  1. Как включить preview Structured Concurrency.

  2. Какие флаги нужны для javac, Maven и Gradle.

  3. Как выглядит ScopedValue для request context.

  4. Как собрать страницу из нескольких источников через StructuredTaskScope.

  5. Как использовать allSuccessfulOrThrow() и anySuccessfulOrThrow().

  6. Как задать timeout на весь scope.

  7. Как ScopedValue и StructuredTaskScope работают вместе.

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

Погнали.


8. Сначала про версии и preview

Virtual threads с Java 21 уже не требуют preview-флагов:

Virtual threads без preview
javac Main.java
java Main

ScopedValue с JDK 25 тоже final. Если вы экспериментируете только с ним, preview-флаги не нужны.

А вот Structured Concurrency пока preview. Значит, нужны флаги и на компиляции, и на запуске.

Для JDK 27 EA с JEP 533:

Preview-флаги для JDK 27
javac --enable-preview --release 27 Main.java
java --enable-preview Main

Для JDK 26:

Preview-флаги для JDK 26
javac --enable-preview --release 26 Main.java
java --enable-preview Main

Для маленьких экспериментов я бы начал именно с одного файла и javac. Меньше магии сборщика, проще понять, где ошибка: в коде, версии JDK или флагах.

Минимальный пример:

Минимальный StructuredTaskScope
import java.util.concurrent.StructuredTaskScope;

public class Main {
    public static void main(String[] args) throws Exception {
        try (var scope = StructuredTaskScope.open()) {
            var hello = scope.fork(() -> "Hello");
            var loom = scope.fork(() -> "Loom");

            scope.join();

            System.out.println(hello.get() + ", " + loom.get());
        }
    }
}

Компиляция для JDK 27 EA:

Компиляция и запуск JDK 27
javac --enable-preview --release 27 Main.java
java --enable-preview Main

Если забыть --enable-preview, компилятор честно напомнит, что вы используете preview API. Ничего загадочного.


9. Maven

Для Maven нужны две части: compiler plugin и surefire, если вы запускаете тесты.

Maven: compiler и surefire
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <release>27</release>
        <compilerArgs>
            <arg>--enable-preview</arg>
        </compilerArgs>
    </configuration>
</plugin>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>--enable-preview</argLine>
    </configuration>
</plugin>

Если проект уже использует <argLine> для JaCoCo или других агентов, не надо его молча затирать. Иначе добавите preview-флаг, а coverage agent пропадет - и CI начнет вести себя странно.

В живом проекте я бы сначала вынес это в профиль:

Maven-профиль loom-preview
<profiles>
    <profile>
        <id>loom-preview</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <release>27</release>
                        <compilerArgs>
                            <arg>--enable-preview</arg>
                        </compilerArgs>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <configuration>
                        <argLine>--enable-preview</argLine>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Запуск:

Запуск Maven-профиля
mvn -Ploom-preview test

Так проще не протащить preview в обычную сборку случайно. Особенно если это не pet project, а нормальный сервис с CI и несколькими разработчиками.


10. Gradle

Для Gradle идея та же: toolchain плюс preview-флаги для компиляции, тестов и запуска.

Gradle Groovy DSL
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(27)
    }
}

tasks.withType(JavaCompile).configureEach {
    options.compilerArgs += "--enable-preview"
}

tasks.withType(Test).configureEach {
    jvmArgs += "--enable-preview"
}

tasks.withType(JavaExec).configureEach {
    jvmArgs += "--enable-preview"
}

Если используете Kotlin DSL:

Gradle Kotlin DSL
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(27))
    }
}

tasks.withType<JavaCompile>().configureEach {
    options.compilerArgs.add("--enable-preview")
}

tasks.withType<Test>().configureEach {
    jvmArgs("--enable-preview")
}

tasks.withType<JavaExec>().configureEach {
    jvmArgs("--enable-preview")
}

Проверка простая: если тесты компилируются, но падают при запуске с ошибкой про preview features, значит флаг добавили только на компиляцию. Такое бывает постоянно. Компиляция и runtime - разные места.


11. Scoped Values: request context без ThreadLocal

Теорию ScopedValue мы уже разобрали в первой части. Здесь - как это выглядит в сервисном коде. Фича уже final с JDK 25, preview-флаги для нее не нужны.

Допустим, в сервисе есть request context:

Модель RequestContext
public record RequestContext(
        String requestId,
        String tenantId,
        String userId
) {
}

Хранилище для scoped value:

Хранилище ScopedValue
import java.lang.ScopedValue;

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

    private RequestContextHolder() {
    }
}

Endpoint или handler связывает значение с областью выполнения:

Binding контекста в handler
import static com.example.RequestContextHolder.REQUEST;

public Response handle(Request request) throws Exception {
    var context = new RequestContext(
            request.id(),
            request.tenantId(),
            request.userId()
    );

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

Ниже по стеку можно получить контекст:

Чтение контекста ниже по стеку
import static com.example.RequestContextHolder.REQUEST;

public List<Order> loadOrders(String userId) {
    RequestContext context = REQUEST.get();

    log.info("load orders, requestId={}, tenant={}",
            context.requestId(),
            context.tenantId());

    return orderClient.load(context.tenantId(), userId);
}

Что важно:

  • после выхода из call() binding исчезает;

  • не нужен finally { remove(); };

  • значение нельзя внезапно переприсвоить в середине операции;

  • если вызвать REQUEST.get() вне области, будет ошибка.

Последний пункт мне нравится. Лучше упасть рядом с причиной, чем потом ловить в логах “невозможное” смешивание контекстов.

Но ScopedValue не стоит использовать везде. Если tenantId нужен бизнес-логике метода, лучше передавать его обычным параметром:

Явная передача tenantId
orderClient.loadRecent(tenantId, userId);

А если это технический контекст для логирования, трассировки, security context и похожих вещей, ScopedValue выглядит хорошо.


12. Первый StructuredTaskScope: собрать страницу пользователя

Теперь Structured Concurrency - та самая preview-часть Loom, которую мы обсуждали в первой части.

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

Сборка UserPage через scope
import java.util.List;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;

public UserPage loadUserPage(String userId) throws Exception {
    try (var scope = StructuredTaskScope.open()) {
        Subtask<User> user =
                scope.fork(() -> userClient.load(userId));

        Subtask<List<Order>> orders =
                scope.fork(() -> orderClient.loadRecent(userId));

        Subtask<List<Recommendation>> recommendations =
                scope.fork(() -> recommendationClient.loadFor(userId));

        scope.join();

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

В этом примере:

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

  • родительский метод явно ждет их через join();

  • после join() можно читать результаты через Subtask.get();

  • когда scope выходит из блока, дочерние задачи не должны остаться бесхозными.

Без structured scope я бы постоянно держал в голове: “а что с остальными future, если первая упала?”. Со scope эта политика становится частью конструкции.

Небольшая практическая деталь: не надо вытаскивать Subtask наружу из метода и передавать дальше по приложению. Подзадача создается внутри scope, и результат нужно читать там же, до закрытия scope.


13. Все задачи одного типа: allSuccessfulOrThrow()

В первой части мы уже видели этот joiner в теории. Здесь - полный рабочий пример.

Иногда задачи однородные. Например, нужно сходить в несколько независимых источников и получить куски одного типа.

Joiner allSuccessfulOrThrow
import java.util.List;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;

public List<String> loadFragments(String documentId) throws Exception {
    try (var scope = StructuredTaskScope.open(
            Joiner.<String>allSuccessfulOrThrow())) {

        scope.fork(() -> fragmentClient.load(documentId, "header"));
        scope.fork(() -> fragmentClient.load(documentId, "body"));
        scope.fork(() -> fragmentClient.load(documentId, "footer"));

        return scope.join();
    }
}

Здесь join() сам возвращает результат joiner’а. Если все подзадачи завершились успешно, получаем List<String>. Если одна упала, scope отменяет оставшиеся задачи, а join() бросает исключение.

Такой код хорошо читается для сценария “или собрали все, или вся операция неуспешна”.

Но я бы не пытался натянуть этот joiner на все случаи. Если результаты разных типов, как в примере с User, Order и Recommendation, удобнее работать с отдельными Subtask<T>. И это нормально. Structured Concurrency не обязана превращать Java в функциональный DSL.


14. Первый успешный ответ: anySuccessfulOrThrow()

Второй joiner из первой части. Частый сценарий: есть несколько зеркал, реплик или провайдеров, и нас устраивает первый успешный ответ.

Joiner anySuccessfulOrThrow
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;

public Price loadPrice(String sku) throws Exception {
    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();
    }
}

Как только есть успешный результат, остальные задачи можно отменять. Если все провайдеры упали, join() сообщает об ошибке.

В старом коде это часто превращалось в самодельные гонки на CompletableFuture.anyOf(), ручные cancel(), обработку исключений и немного магии вокруг типов. Здесь политика названа прямо: anySuccessfulOrThrow().

Тут легко забыть про идемпотентность и стоимость отмены. Если задача уже отправила внешний запрос, отмена Java-потока не обязательно мгновенно отменит работу на другой стороне. Это не минус Structured Concurrency, это реальность распределенных систем. Timeout и cancellation на клиенте должны сочетаться с timeout’ами HTTP-клиента, драйвера БД и других внешних вызовов.


15. Timeout как часть scope

Timeout - это место, где особенно видно отличие структурного подхода от “я где-то поставил таймер”.

Timeout для search scope
import java.time.Duration;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;

public SearchResult search(String query) throws Exception {
    try (var scope = StructuredTaskScope.open(
            Joiner.<SearchResult>anySuccessfulOrThrow(),
            cfg -> cfg
                    .withName("search")
                    .withTimeout(Duration.ofMillis(300)))) {

        scope.fork(() -> searchClientA.search(query));
        scope.fork(() -> searchClientB.search(query));
        scope.fork(() -> searchClientC.search(query));

        return scope.join();
    }
}

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

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

Но тут тоже без иллюзий. Timeout scope не заменяет:

  • timeout HTTP-клиента;

  • timeout запроса к БД;

  • лимит connection pool;

  • circuit breaker;

  • нормальную обработку InterruptedException.

Structured Concurrency управляет жизненным циклом группы задач. На внешние системы это не влияет напрямую.


16. Scoped Values вместе со StructuredTaskScope

Virtual threads, Scoped Values и Structured Concurrency в первой части мы разбирали по отдельности. Здесь - полный практический пример их связки. Дочерние задачи, созданные внутри StructuredTaskScope, наследуют scoped values родительской операции.

Например, endpoint собирает страницу пользователя из нескольких сервисов. Мы не хотим протаскивать RequestContext через каждый метод руками, но и глобальную магию с ThreadLocal тащить не хочется.

ScopedValue вместе со scope
import java.lang.ScopedValue;
import java.util.List;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;

public final class UserPageService {
    private static final ScopedValue<RequestContext> REQUEST =
            ScopedValue.newInstance();

    public UserPage handle(Request request) throws Exception {
        var context = new RequestContext(
                request.id(),
                request.tenantId(),
                request.userId()
        );

        return ScopedValue
                .where(REQUEST, context)
                .call(() -> loadUserPage(request.userId()));
    }

    private UserPage loadUserPage(String userId) throws Exception {
        try (var scope = StructuredTaskScope.open()) {
            Subtask<User> user =
                    scope.fork(() -> loadUser(userId));

            Subtask<List<Order>> orders =
                    scope.fork(() -> loadOrders(userId));

            Subtask<List<Recommendation>> recommendations =
                    scope.fork(() -> loadRecommendations(userId));

            scope.join();

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

    private User loadUser(String userId) {
        RequestContext context = REQUEST.get();
        return userClient.load(context.tenantId(), userId);
    }

    private List<Order> loadOrders(String userId) {
        RequestContext context = REQUEST.get();
        return orderClient.loadRecent(context.tenantId(), userId);
    }

    private List<Recommendation> loadRecommendations(String userId) {
        RequestContext context = REQUEST.get();
        return recommendationClient.loadFor(context.tenantId(), userId);
    }
}

Тут контекст живет ровно столько, сколько обрабатывается запрос. Подзадачи видят тот же tenantId и requestId, но не получают права менять binding. Когда handle() завершился, контекста больше нет.

StructuredTaskScope управляет жизненным циклом дочерних задач, а ScopedValue ограничивает время жизни контекста. С ThreadLocal контекст приходится очищать вручную через remove(). Здесь граница видна прямо в коде.


17. Несколько практических правил

Я бы не начинал с переписывания всего сервиса. Лучше взять один маленький сценарий: параллельные запросы в несколько downstream-сервисов, агрегация данных, поиск первого успешного ответа, экспериментальный endpoint.

Дальше смотреть на поведение:

  • что происходит при ошибке одной подзадачи;

  • отменяются ли остальные;

  • не остаются ли хвосты во внешних системах;

  • что показывают логи и thread dump;

  • не потеряли ли вы backpressure после перехода с fixed pool на virtual threads;

  • где должны жить timeout’ы: на scope, HTTP-клиенте, БД, gateway.

И еще одно. Preview API лучше не добавлять в публичную библиотеку: при смене preview-версии ее будет сложнее обновлять. В приложении экспериментировать проще: обновили JDK, поправили несколько мест, поехали дальше. В стабильном публичном API preview-зависимость может стать неприятным долгом.

Structured Concurrency сейчас лучше изучать на небольших примерах: писать прототипы, проверять поведение на реальных сценариях и следить за JEP.


18. Частые ошибки

Первая ошибка: забыли --enable-preview на runtime. Компиляция прошла, тесты или запуск упали. Лечится добавлением флага в Test, JavaExec, surefire или команду java.

Вторая: перепутали версии JDK и примеры из старых preview. У Structured Concurrency API менялся. Код из статьи под JDK 21 может не совпасть с JDK 25, 26 или 27.

Третья: решили, что virtual threads сами заменяют backpressure. Нет. Если внешний сервис выдерживает 50 запросов, надо явно ограничивать параллелизм.

Четвертая: положили в ScopedValue mutable map “на все случаи жизни”. Получился старый глобальный контекст, только в новой упаковке.

Пятая: решили, что cancel в Java мгновенно отменяет реальную работу во внешнем мире. Иногда да, иногда нет. Проверяйте клиент, драйвер, протокол и timeout’ы.


Вывод

Практически Project Loom сейчас выглядит так:

  • virtual threads уже можно использовать без preview-флагов;

  • ScopedValue с JDK 25 тоже final;

  • Structured Concurrency все еще preview, поэтому требует --enable-preview и аккуратности с версиями JDK.

Самая полезная связка для backend-кода выглядит так: один запрос получает свой bounded context через ScopedValue, внутри него создается StructuredTaskScope, дочерние задачи наследуют контекст, выполняются параллельно и завершаются вместе с родительской операцией.

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


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


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