Структурированный параллелизм (Structured Concurrency)
Тема дорожной карты · Java
Структурированный параллелизм (structured concurrency) — API из Project Loom, переносящий принцип структурного программирования на многопоточность: группа связанных подзадач запускается, живёт и завершается строго внутри одного блока кода. Впервые появился как preview в Java 21 (JEP 453), а в Java 25 (JEP 505, пятый preview) API радикально переработали: вместо публичных конструкторов и вложенных классов ShutdownOnFailure/ShutdownOnSuccess теперь есть статическая фабрика StructuredTaskScope.open() и интерфейс Joiner, задающий политику завершения. Шестой и седьмой preview (JEP 525 в Java 26 и JEP 533 в Java 27) шлифуют детали, но форма API держится. Функциональность всё ещё preview: для компиляции и запуска нужен флаг --enable-preview.
Как это работает
Scope открывается в try-with-resources, подзадачи создаются через fork() — каждая в новом виртуальном потоке, а join() ждёт итог по выбранной политике:
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(() -> userService.find(id)); // Subtask<User>
var order = scope.fork(() -> orderService.find(id)); // Subtask<Order>
scope.join(); // ждёт всех; при сбое одной — отменяет остальные
return render(user.get(), order.get());
}
Политика по умолчанию: все подзадачи обязаны завершиться успешно. Если одна упала, остальные отменяются, а join() бросает FailedException с исходной причиной — ошибка не теряется. Другие политики задаются реализациями Joiner: например, StructuredTaskScope.Joiner.anySuccessfulResultOrThrow() вернёт из join() первый успешный результат (гонка зеркальных источников), а awaitAll() дождётся всех независимо от исхода. Ключевые гарантии модели: подзадача не может пережить свой scope, отмена распространяется каскадом на всю иерархию вложенных областей, а в дампе потоков видно дерево «родитель — подзадачи», а не плоский список, что сильно упрощает диагностику.
Мотивация модели: при ручном запуске фоновых задач родитель может завершиться раньше своих детей, отмена легко забывается, а исключение из фоновой задачи теряется в никуда. Структурная модель устраняет целый класс подобных утечек синтаксически — выйти из блока, оставив живую подзадачу, просто невозможно. Дополнительно перегрузка open() принимает конфигурацию, в которой задаётся таймаут на весь блок: по его истечении незавершённые подзадачи отменяются так же, как при сбое.
Когда применять
- Fan-out: параллельные вызовы нескольких сервисов, когда ответ нужен целиком, а при сбое любого продолжать бессмысленно.
- Гонка эквивалентных источников: берём первый успешный ответ, остальные автоматически отменяем.
- Как читаемая замена цепочек
CompletableFuture, где отмену и обработку ошибок приходится собирать вручную. - В связке с виртуальными потоками: тысячи дешёвых подзадач в понятной иерархии вместо ручного управления пулами.
Типичные ошибки
- Копировать примеры 2023–2024 годов из статей:
new StructuredTaskScope.ShutdownOnFailure()удалён из API и не компилируется на JDK 25+. - Забыть
--enable-previewпри компиляции и запуске — код не соберётся. - Вызывать
fork()послеjoin()или из постороннего потока: это нарушение структуры, scope бросит исключение. - Считать scope пулом потоков и пытаться переиспользовать: он одноразовый и создаётся на каждый блок работы.
- Затаскивать preview-API в публичные библиотеки: между релизами API уже менялся радикально, обратной совместимости никто не обещал.
Связанные понятия
Полезные ресурсы
Проверить знания (2)
Загрузка вопросов…