Skip to main content

JEP 533: Structured Concurrency (Seventh Preview)

Introduction

We have long taken structured control flow for granted: if, for, try blocks nesting within one another, with entry and exit points that are perfectly clear. But the moment we step into concurrency, that clarity vanishes: tasks are flung into a thread pool, returning a pile of unrelated futures that no one governs. JEP 533's structured concurrency exists to bring that orderly block structure of control flow back into the concurrent world.

Unstructured concurrency leaks

Think about how we usually write concurrency: submit tasks to an ExecutorService, get back a pile of Futures. This model looks flexible but hides real hazards.

  • The Futures an ExecutorService hands back have no lifetime relationship with the caller. The method returns while the future may still be running in the background; the future's lifetime is completely detached from the block that created it.
  • When one subtask fails, its siblings keep running and burning resources. No one tells them "one of you has already failed, you can stop now."
  • Cancellation must be threaded through by hand. You have to manually pass the cancellation signal down level by level, and that manual threading often gets lost, one link forgets to handle it and cancellation silently fails.
  • Stack traces and thread dumps lose the caller/subtask relationship. When something goes wrong and you look at a thread dump, you see a pile of isolated threads with no way to tell who forked whom or who is waiting on what. The parent-child relationship is gone entirely.

In a word: unstructured concurrency leaks, tasks, resources, and context all leak.

The structured idea

The idea behind structured concurrency is reassuringly straightforward: make concurrent task lifetimes nest like code blocks.

  • Tasks split into subtasks inside a scope, and the scope owns them.
  • After join, leaving the scope automatically reins in any unfinished subtasks, bringing them back under control so nothing sneaks off to keep running.
  • Subtask lifetimes nest inside the calling block, like a code block. The parent block cannot end while a subtask is still alive.
  • That parent-child relationship is visible to the runtime and to tooling. It is not just a mental model in the programmer's head but a structure the runtime genuinely understands and can surface in diagnostic tools.

The API shape

In terms of the API, structured concurrency reads very naturally in Java:

  • Open a scope with StructuredTaskScope.open(...) inside a try-with-resources block.
  • Start subtasks with scope.fork(...), each on its own virtual thread.
  • Wait for the outcome the policy defines with scope.join().
  • The policy is expressed by a Joiner: you can require all to succeed, any to succeed, or define your own rules.
try (var scope = StructuredTaskScope.open(
Joiner.allSuccessfulOrThrow())) {
Subtask<String> user = scope.fork(() -> findUser());
Subtask<Order> order = scope.fork(() -> fetchOrder());
scope.join();
return new Response(user.get(), order.get());
}

This code opens a scope, forks two subtasks, one to find the user, one to fetch the order, then joins. The Joiner.allSuccessfulOrThrow() policy means: if either subtask fails, the other is automatically cancelled and an exception is thrown. The entire concurrent logic is bounded by the try block, so no subtask can leak outside it.

Before and after

ExecutorService (the traditional way):

  • Futures outlive the method that created them.
  • Errors surface late, at get() time, often too late.
  • Cancellation and cleanup are manual, and therefore can be forgotten at any point.

StructuredTaskScope (structured concurrency):

  • Subtasks cannot outlive their scope. They are physically bounded by the try block's borders.
  • Failure, success, and short-circuiting are all defined by the Joiner policy.
  • Short-circuiting and cleanup come from the joiner, no longer something you write by hand.

The core difference: in the traditional model, subtasks can escape; with structured concurrency, they simply cannot.

Preview status

This is already the seventh preview, a fairly high number that tells you the API has been through extensive refinement and repeated reshaping.

  • It ships in JDK 27 as a seventh preview, with the API reshaped along the way.
  • It requires the --enable-preview flag.
  • It is designed hand in hand with Project Loom's virtual threads: virtual threads make it cheap to fork thousands of subtasks without a second thought, and structured concurrency makes sure all those subtasks are properly managed and never spin out of control. The two complement each other.

Conclusion

Structured concurrency makes concurrent task lifetimes as clear and visible as code blocks. Subtasks are firmly bounded by their scope, they cannot leak and cannot be forgotten. This not only makes concurrent code easier to write and read but also easier to debug when things go wrong: thread dumps can finally show who is whose parent.