Що таке 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]]