Что такое structured concurrency?
Structured Concurrency (структурированная конкурентность) — это модель программирования, где группа связанных задач, выполняющихся в разных потоках, рассматривается как единый б...
Structured Concurrency (структурированная конкурентность) — это модель программирования, где группа связанных задач, выполняющихся в разных потоках, рассматривается как единый блок работы с чёткими границами. Ключевая идея: если задача породила дочерние задачи, она обязана дождаться их завершения. Это аналог try-with-resources для многопоточности.
Почему это важно: в классической многопоточности потоки живут “сами по себе” — родительский метод может завершиться, а порождённые потоки продолжат работать (или утекут). Structured Concurrency гарантирует: к моменту выхода из блока все дочерние задачи завершены — либо успешно, либо с ошибкой.
Важная оговорка: StructuredTaskScope доступен начиная с Java 21 как Preview API. Это значит, что API может измениться в будущих версиях. Для production-использования на Java 17- используйте ExecutorService + CompletableFuture.
Junior уровень
Базовое понимание
Structured Concurrency (структурированная конкурентность) — это модель программирования, где группа связанных задач, выполняющихся в разных потоках, рассматривается как единый блок работы с чёткими границами.
Почему классический подход проблематичен: когда вы создаёте потоки через new Thread().start(), они становятся “сиротами” — родительский метод не знает об их статусе, не может отменить их при ошибке, и не гарантирует их завершение. Structured Concurrency решает это через принцип: “родитель ждёт детей”.
Проблема классической многопоточности
// Потоки-"сироты" — запустил и забыл
public void process() {
Thread t1 = new Thread(() -> fetchUserData());
Thread t2 = new Thread(() -> fetchOrderHistory());
t1.start();
t2.start();
// Что если t1 упал с ошибкой? t2 продолжит работать!
// Что если основной метод упал? Потоки останутся!
}
Решение: StructuredTaskScope (Java 21+ Preview)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<User> user = scope.fork(() -> fetchUser(id));
Subtask<Order> orders = scope.fork(() -> fetchOrders(id));
scope.join(); // Ждём все задачи
scope.throwIfFailed(); // Если одна упала — остальные отменены
// Оба результата доступны
return new Response(user.get(), orders.get());
}
// К моменту выхода из try-with-resources все потоки завершены!
Аналогия
Классические потоки: Structured Concurrency:
Родитель Родитель
│ │
├→ Поток 1 (сирота) ├→ Задача 1 (дочерняя)
├→ Поток 2 (сирота) ├→ Задача 2 (дочерняя)
└→ Завершается └→ Ждёт детей
│
Все дети завершены → Родитель завершается
Middle уровень
Политики завершения (Shutdown Policies)
1. ShutdownOnFailure
Если хотя бы одна подзадача упала — остальные отменяются:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<Data> data = scope.fork(() -> fetchData());
Subtask<Config> config = scope.fork(() -> fetchConfig());
scope.join();
scope.throwIfFailed();
// Если fetchData() упал — fetchConfig() автоматически отменён
process(data.get(), config.get());
}
Когда: Параллельное выполнение шагов одного бизнес-процесса.
2. ShutdownOnSuccess
Как только первая задача вернула результат — остальные отменяются:
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
scope.fork(() -> fetchFromServer1(url));
scope.fork(() -> fetchFromServer2(url));
scope.fork(() -> fetchFromServer3(url));
scope.join();
String fastestResult = scope.result(); // Результат первого завершённого
// Остальные запросы автоматически отменены!
}
Когда: Запросы к зеркалам (кто быстрее ответит).
Сравнение с ExecutorService
| Характеристика | ExecutorService | StructuredTaskScope |
|---|---|---|
| Жизненный цикл | Не ограничен областью видимости | Строго внутри try-with-resources |
| Управление ошибками | Ручное (через Future) | Автоматическое (Policy-based) |
| Отмена | Ручная (future.cancel()) |
Автоматическая (каскадная) |
| Context Propagation | Сложно (нужны обёртки) | Поддерживает Scoped Values |
| Утечки потоков | Возможны | Невозможны |
Error Propagation
// ExecutorService — ошибка "тихо умирает"
Future<?> future = executor.submit(() -> {
throw new RuntimeException("Ошибка!");
});
// Никто не узнает, пока не вызовете future.get()
// StructuredTaskScope — ошибка летит к родителю
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
scope.fork(() -> {
throw new RuntimeException("Ошибка!");
});
scope.join();
scope.throwIfFailed(); // → ExecutionException автоматически
}
Почему это революция в отладке?
Наблюдаемость (Observability)
Дампы потоков теперь иерархичны:
jcmd <pid> Thread.dump_to_file -format=json threads.json
{
"threads": [
{
"name": "main",
"children": [
{
"name": "Scope-1/Worker-1",
"task": "fetchUser"
},
{
"name": "Scope-1/Worker-2",
"task": "fetchOrders"
}
]
}
]
}
Senior уровень
Under the Hood: Как работает StructuredTaskScope
// Упрощённо
public abstract class StructuredTaskScope<T> implements AutoCloseable {
private final ThreadFactory factory;
private final Set<Subtask<T>> subtasks = ConcurrentHashMap.newKeySet();
private volatile boolean closed = false;
public <U extends T> Subtask<U> fork(Callable<? extends U> task) {
if (closed) throw new IllegalStateException("Scope closed");
Thread thread = factory.newThread(() -> {
try {
U result = task.call();
handleSuccess(result);
} catch (Throwable t) {
handleFailure(t);
}
});
thread.start();
// ...
}
@Override
public void close() {
// Гарантирует: все подзадачи завершены к моменту выхода
join();
}
}
Resource Safety
Модель гарантирует, что к моменту выхода из try все потоки завершены:
// НЕВОЗМОЖНО:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
scope.fork(() -> longRunningTask());
return; // ← Компилятор не даст! Scope должен быть закрыт
}
// К моменту выхода из } — все задачи завершены
Интеграция с Virtual Threads
// StructuredTaskScope спроектирован для Virtual Threads
try (var scope = new StructuredTaskScope.ShutdownOnFailure>(
Thread.ofVirtual().factory())) {
// Создаём миллион подзадач — VT масштабируются
for (int i = 0; i < 1_000_000; i++) {
scope.fork(() -> processItem(i));
}
scope.join();
}
// Создание scope на каждый запрос — дешёвая операция
Scoped Values Integration
// Передача контекста без ThreadLocal оверхеда
static final ScopedValue<UserContext> CONTEXT = new ScopedValue<>();
try (var scope = new StructuredTaskScope.ShutdownOnFailure>()) {
ScopedValue.runWhere(CONTEXT, userContext, () -> {
scope.fork(() -> {
// CONTEXT.get() доступен во всех подзадачах
return fetchUser(CONTEXT.get().userId());
});
scope.fork(() -> {
return fetchOrders(CONTEXT.get().userId());
});
});
scope.join();
}
Диагностика
JSON Thread Dump
# Только этот формат показывает иерархию!
jcmd <pid> Thread.dump_to_file -format=json threads.json
# Обычный текстовый дамп иерархию НЕ показывает
jstack <pid>
Programmatically
try (var scope = new StructuredTaskScope.ShutdownOnFailure>()) {
Subtask<?> task = scope.fork(() -> doWork());
// Проверка статуса
System.out.println(task.state()); // RUNNING, SUCCESS, FAILED
scope.join();
if (task.state() == Subtask.State.SUCCESS) {
System.out.println(task.get());
}
}
Когда использовать
| Сценарий | StructuredTaskScope | ExecutorService |
|---|---|---|
| Бизнес-транзакция (несколько шагов) | Да | Нет |
| Запрос к зеркалам | Да (ShutdownOnSuccess) | Нет |
| Долгоживущий фоновый сервис | Нет | Да |
| Непрерывная обработка очереди | Нет | Да |
| Параллельные независимые задачи | Да | Да |
Best Practices
- Используйте для бизнес-транзакций — когда несколько шагов должны завершиться вместе
- ShutdownOnFailure — когда все шаги обязательны
- ShutdownOnSuccess — для запросов к зеркалам
- Комбинируйте с VT — для масштабирования
- Scoped Values — для передачи контекста вместо ThreadLocal
- try-with-resources — гарантия завершения всех потоков
- JSON thread dump — для отладки иерархии
- Не для долгоживущих сервисов — используйте ExecutorService
Когда НЕ использовать Structured Concurrency
- Долгоживущие фоновые сервисы (демоны, обработчики очередей) — StructuredTaskScope требует завершения всех задач при выходе из блока. Для “вечных” задач нужен ExecutorService
- Java ниже 21 — API в preview, может измениться. Используйте CompletableFuture + ExecutorService
- Независимые параллельные задачи без общей транзакции — если задачи действительно независимы, проще использовать
executor.submit()без overhead scope - Нужен полный контроль над жизненным циклом — если вам нужно вручную отменять, приостанавливать, возобновлять задачи, ExecutorService гибче
StructuredTaskScope vs ExecutorService vs CompletableFuture: что выбрать?
| Ситуация | Выбор | Почему |
|---|---|---|
| Бизнес-транзакция (fetchUser + fetchOrders вместе) | StructuredTaskScope | Автоотмена при ошибке, гарантия завершения |
| Запрос к зеркалам (кто быстрее) | StructuredTaskScope.ShutdownOnSuccess | Автоотмена медленных |
| Фоновая обработка очереди | ExecutorService | Бесконечный цикл, scope не подходит |
| Callback-цепочка (thenApply, thenCompose) | CompletableFuture | Асинхронный pipeline, не блокирует |
| Параллельные независимые задачи | Любой | Но StructuredTaskScope проще для отладки |
🎯 Шпаргалка для интервью
Обязательно знать:
- Structured Concurrency: группа задач = единый блок; родитель ждёт завершения всех дочерних задач
- Аналог try-with-resources для многопоточности: к выходу из блока все потоки гарантированно завершены
- ShutdownOnFailure: одна упала — остальные отменяются (бизнес-транзакции)
- ShutdownOnSuccess: первая завершилась — остальные отменяются (запросы к зеркалам)
StructuredTaskScope— Preview API в Java 21, может измениться- Интеграция с Virtual Threads и Scoped Values для масштабируемости и передачи контекста
- Отличие от ExecutorService: нет утечек потоков, автоматическая отмена, иерархический thread dump
Частые уточняющие вопросы:
- Почему это лучше ExecutorService + CompletableFuture? — ExecutorService: потоки-“сироты”, ручная отмена, возможна утечка. SC: автоматическое управление жизненным циклом, каскадная отмена, невозможность утечки
- Как SC работает с Virtual Threads? — SC создаётся с
Thread.ofVirtual().factory()— все fork-задачи выполняются на VT, масштабируются до миллионов - Когда НЕ использовать SC? — Для долгоживущих фоновых сервисов (демоны, обработчики очередей) — SC требует завершения при выходе из блока
- Как отлаживать SC? — JSON thread dump (
jcmd Thread.dump_to_file -format=json) показывает иерархию родитель-дети
Красные флаги (НЕ говорить):
- ❌ “Structured Concurrency — это стабильный API в Java 21” —
StructuredTaskScopeвсё ещё в Preview - ❌ “SC заменяет ExecutorService полностью” — SC для бизнес-транзакций, ExecutorService для долгоживущих сервисов
- ❌ “SC автоматически обрабатывает все исключения” — нужно вызвать
throwIfFailed()послеjoin() - ❌ “Можно вернуть результат из блока SC до его завершения” — блок SC должен быть закрыт (try-with-resources), все задачи должны завершиться до выхода
Связанные темы:
- [[23. Что такое Virtual Threads в Java 21]]
- [[24. В чём преимущества Virtual Threads перед обычными потоками]]
- [[25. Когда стоит использовать Virtual Threads]]
- [[28. Что такое Callable и Future]]