Вопрос 25 · Раздел 9

Что такое structured concurrency?

Structured Concurrency (структурированная конкурентность) — это модель программирования, где группа связанных задач, выполняющихся в разных потоках, рассматривается как единый б...

Версии по языкам: English Russian Ukrainian

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

  1. Используйте для бизнес-транзакций — когда несколько шагов должны завершиться вместе
  2. ShutdownOnFailure — когда все шаги обязательны
  3. ShutdownOnSuccess — для запросов к зеркалам
  4. Комбинируйте с VT — для масштабирования
  5. Scoped Values — для передачи контекста вместо ThreadLocal
  6. try-with-resources — гарантия завершения всех потоков
  7. JSON thread dump — для отладки иерархии
  8. Не для долгоживущих сервисов — используйте 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]]