Structured Concurrency in Java

Naveen AnamaneniUpdated · 5 min read

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.

Subtasks live and die inside the scope's block

Methods

MethodDescription
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.

Sc.java
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:

Terminal
java --enable-preview --source 25 Sc.java
Terminal output
Navika scored 91
Both in parallel (under 450 ms)? true
Failed: java.lang.IllegalStateException: rank service down

Explanation:

  • fork starts 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, fetchRank throws. The default policy makes join() throw FailedException with the original error as its cause, and cancels the still-running fetchName.
  • When each try block ends, no subtask can still be running.

Without --enable-preview, the compiler stops with errors such as:

Compiler output
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

  1. Easy: Add a third lookup, fetchCity, to the first scope and print all three results.
  2. Harder: Use StructuredTaskScope.open(StructuredTaskScope.Joiner.anySuccessfulResultOrThrow()) to query two "mirror" services and return whichever answers first.

Quick check: Structured concurrency

4 questions
1What is the status of StructuredTaskScope in Java 25?
2What does fork() do?
3With the default policy, what happens when one subtask fails?
4When can a subtask still be running after the scope's block ends?

Key Takeaways

Was this tutorial helpful?

Lesson 432 of 482