1. What Is Structured Concurrency?
The Problem (Before Java 21)
Traditionally, Java concurrency used tools like:
-
Thread,ExecutorService,Future, andCompletableFuture. - You’d manually create threads, track them, and handle cancellations or exceptions yourself.
The problem?
It’s hard to manage lifecycles. Threads could outlive their parent logic.
Exceptions might get lost, and cancellation is messy.
Example:
Future<String> f1 = executor.submit(this::task1);
Future<String> f2 = executor.submit(this::task2);
String result1 = f1.get(); // blocks
String result2 = f2.get();
If one fails, the other still runs. You must remember to cancel or handle it.
That’s unstructured concurrency.
The Solution: Structured Concurrency (JEP 453 / Java 21+)
Structured concurrency treats concurrent subtasks as structured blocks,
just like try-with-resources or nested method calls.
- All threads are started, joined, and canceled together.
- The parent waits for all subtasks to finish.
- Exceptions are automatically aggregated and propagated.
Think of it as:
“Concurrency with clean lifecycles — all children finish before the parent exits.”
2. Key Concepts
| Concept | Description |
|---|---|
StructuredTaskScope |
A scope that manages subtasks together. It starts, joins, cancels, and aggregates results. |
Subtask |
A unit of work submitted to the scope via fork(). |
Joiner |
Defines how the scope joins results — for example, stop after any success, all success, or a custom condition. |
Structured Concurrency |
The programming model — ensures that concurrent tasks are managed as a structured whole. |
Virtual Threads |
Lightweight threads from Project Loom — perfect for structured concurrency since they scale to millions of tasks efficiently. |
3. StructuredTaskScope in Action
The class StructuredTaskScope manages subtasks:
try (var scope = StructuredTaskScope.open()) {
var t1 = scope.fork(() -> task("A"));
var t2 = scope.fork(() -> task("B"));
scope.join(); // Wait for all subtasks to finish
System.out.println(t1.get());
System.out.println(t2.get());
}
When you exit the try block:
- All threads are joined.
- If any exception occurred, it’s handled properly.
- The scope cleans itself up.
4. Built-in Joiners
allSuccessfulOrThrow()
Waits for all subtasks to succeed or throws an exception.
try (var scope = StructuredTaskScope.open(allSuccessfulOrThrow())) {
var t1 = scope.fork(() -> task(2, false));
var t2 = scope.fork(() -> task(3, true)); // this fails
scope.join();
}
If any task fails → it throws an exception after joining.
anySuccessfulResultOrThrow()
Waits for the first successful task, cancels the rest.
try (var scope = StructuredTaskScope.open(anySuccessfulResultOrThrow())) {
scope.fork(() -> task(4, false));
scope.fork(() -> task(2, true));
scope.fork(() -> task(3, false));
Object result = scope.join();
System.out.println(result);
}
As soon as one subtask returns successfully, others are canceled.
Custom Joiner (like TargetSuccessJoiner)
You can define your own joining logic using StructuredTaskScope.Joiner.
5. Custom Joiner: TargetSuccessJoiner
Let’s understand your custom joiner step-by-step:
static class TargetSuccessJoiner<T> implements StructuredTaskScope.Joiner<T, List<T>> {
private final int targetCount;
private final List<T> successfulResults = Collections.synchronizedList(new ArrayList<>());
private final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<>());
TargetSuccessJoiner(int targetCount) {
this.targetCount = targetCount;
}
targetCount = how many successful results you want before stopping.
Example: You may only need 2 successful responses from 10 servers.
The onComplete() Method
Called when each subtask finishes:
@Override
public boolean onComplete(StructuredTaskScope.Subtask<? extends T> subtask) {
switch (subtask.state()) {
case SUCCESS -> {
System.out.println("Success: " + subtask.get());
successfulResults.add(subtask.get());
if (successfulResults.size() >= targetCount) {
System.out.println("🚦 Reached " + targetCount + " successes — shutting down scope!");
return true; // stop scope early
}
}
case FAILED -> {
System.out.println("Failure: " + subtask.exception());
exceptions.add(subtask.exception());
}
}
return false; // continue waiting
}
Key points:
- Each subtask completion is observed.
- When enough successes are reached, it ends early.
- Failed subtasks are recorded.
The result() Method
Called after all tasks have completed or stopped early.
@Override
public List<T> result() throws Throwable {
if (successfulResults.size() >= targetCount) {
return List.copyOf(successfulResults);
} else {
Exception ex = new Exception("Fewer than " + targetCount + " tasks succeeded");
exceptions.forEach(ex::addSuppressed);
throw ex;
}
}
If not enough successes, it throws an exception with all suppressed errors attached.
6. Full Example: twoSuccessfulJoiner()
private static void twoSuccessfulJoiner() {
try (var scope = StructuredTaskScope.open(new TargetSuccessJoiner<String>(2))) {
scope.fork(() -> task(4, false));
scope.fork(() -> task(3, false));
scope.fork(() -> task(2, true));
scope.fork(() -> task(5, false));
List<String> results = scope.join();
System.out.println("Collected: " + results);
} catch (Exception ex) {
ex.printStackTrace();
}
}
Behavior:
- Four tasks run concurrently.
- Stop after 2 successes.
- Failures are logged but do not prevent others from running.
-
join()returns a list of successful results.
7. Example Task Method
private static String task(int sec, boolean fail) {
try {
TimeUnit.SECONDS.sleep(sec);
if (fail) throw new RuntimeException("Task Failed");
return String.format("Task finished in %s sec", sec);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
This simulates a slow task — either success or failure after a delay.
8. With Timeout Example
try (var scope = StructuredTaskScope.open(allSuccessfulOrThrow(),
cf -> cf.withTimeout(Duration.ofSeconds(4)))) {
var t1 = scope.fork(() -> task(4, false));
var t2 = scope.fork(() -> task(5, false));
scope.join();
}
If subtasks don’t finish in time, the scope cancels them automatically.
9. Structured Concurrency + Virtual Threads
Structured Concurrency works beautifully with Virtual Threads (Project Loom).
Virtual Threads Recap:
- Introduced in Java 21.
- Extremely lightweight — you can spawn millions.
- Perfect for I/O-bound concurrent tasks.
Example:
try (var scope = StructuredTaskScope.open()) {
var task1 = scope.fork(() -> Thread.ofVirtual().factory().newThread(() -> task(2, false)));
var task2 = scope.fork(() -> task(3, false));
scope.join();
}
In fact, StructuredTaskScope automatically uses virtual threads by default (in most modern JDK versions).
So you get massive scalability + clean structure.
10. Why Use Structured Concurrency?
| Benefit | Description |
|---|---|
| Simplicity | One scope manages all child tasks. |
| Safety | No thread leaks, no forgotten joins. |
| Propagated Exceptions | All exceptions bubble up cleanly. |
| Cancellable | You can stop early (like with anySuccessfulResultOrThrow or custom logic). |
| Works with Virtual Threads | Efficient and scalable. |
Final Thoughts
Structured concurrency brings clarity and safety to concurrent Java code:
- You can now treat parallel work as structured blocks.
- It’s composable, maintainable, and easy to reason about.
Quick Summary
| Concept | Purpose |
|---|---|
StructuredTaskScope |
Parent scope for concurrent subtasks |
Subtask |
Individual child computation |
Joiner |
Strategy for combining or short-circuiting results |
Virtual Threads |
Lightweight concurrency engine |
| Custom Joiner | Define advanced success/failure logic |
Top comments (0)