Structured Concurrency in Java
Structured concurrency is a way of writing concurrent code so that related subtasks are treated as one unit of work: they start together inside a block, and the block doesn't finish until all of them have finished. If one subtask fails, the others are canceled automatically, and no thread is left running in the background. Java provides it through StructuredTaskScope. In Java 25 it is a preview feature (JEP 505), so it must be enabled with --enable-preview, and its API may still change. In this lesson, you will learn how to use it and why it's safer than starting tasks with a plain executor.
The Problem It Solves
With an ExecutorService, if you submit two subtasks and one fails, the other keeps running unless you remember to cancel it; if the calling thread is interrupted, the subtasks don't know. Structured concurrency ties the subtasks' lifetimes to a block of code, just as a method call's lifetime is tied to its caller.
Methods
| Method | Description |
|---|---|
StructuredTaskScope.open() | It is used to open a scope that fails as soon as any subtask fails (the default policy). |
StructuredTaskScope.open(joiner) | It opens a scope with another policy, such as "first success wins". |
scope.fork(callable) | It starts a subtask in a new virtual thread and returns a Subtask. |
scope.join() | It waits for the subtasks according to the policy; throws FailedException on failure. |
subtask.get() | It returns the subtask's result after join(). |
scope.close() | It ensures all subtasks are finished (called by try-with-resources). |
Example: Two Lookups in Parallel
Let's see an example that fetches a student's name and marks in parallel, then shows what happens when one lookup fails.
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
public class Sc {
static String fetchName() throws InterruptedException {
Thread.sleep(200);
return "Navika";
}
static int fetchMarks() throws InterruptedException {
Thread.sleep(300);
return 91;
}
static int fetchRank() {
throw new IllegalStateException("rank service down");
}
public static void main(String[] args) throws Exception {
long start = System.nanoTime();
try (var scope = StructuredTaskScope.open()) {
Subtask<String> name = scope.fork(Sc::fetchName);
Subtask<Integer> marks = scope.fork(Sc::fetchMarks);
scope.join();
System.out.println(name.get() + " scored " + marks.get());
}
System.out.println("Both in parallel (under 450 ms)? "
+ ((System.nanoTime() - start) / 1_000_000 < 450));
try (var scope = StructuredTaskScope.open()) {
Subtask<String> name = scope.fork(Sc::fetchName);
Subtask<Integer> rank = scope.fork(Sc::fetchRank);
scope.join();
} catch (StructuredTaskScope.FailedException e) {
System.out.println("Failed: " + e.getCause());
}
}
}Because the API is a preview, it must be enabled when running the program:
java --enable-preview --source 25 Sc.javaNavika scored 91
Both in parallel (under 450 ms)? true
Failed: java.lang.IllegalStateException: rank service downExplanation:
forkstarts each lookup in its own virtual thread; both sleep at the same time, so the whole scope takes about 300 milliseconds, not 500.join()waits for both. After it returns,get()gives each result.- In the second scope,
fetchRankthrows. The default policy makesjoin()throwFailedExceptionwith the original error as its cause, and cancels the still-runningfetchName. - When each
tryblock ends, no subtask can still be running.
Without --enable-preview, the compiler stops with errors such as:
Sc.java:1: error: StructuredTaskScope is a preview API and is disabled by default.
import java.util.concurrent.StructuredTaskScope;
^
(use --enable-preview to enable preview APIs)A Preview API
Common Mistakes
Calling subtask.get() before join(). It throws IllegalStateException.
Forking outside the try-with-resources block. The scope must own the subtasks for their whole life.
Forgetting --enable-preview. The code doesn't compile.
Practice
- Easy: Add a third lookup,
fetchCity, to the first scope and print all three results. - Harder: Use
StructuredTaskScope.open(StructuredTaskScope.Joiner.anySuccessfulResultOrThrow())to query two "mirror" services and return whichever answers first.
Key Takeaways
Was this tutorial helpful?