With Java 24, Structured Concurrency moves closer to becoming a first-class feature in the Java platform. This is currently a preview language feature.
Traditional concurrency in Java often results in fragmented and error-prone code, where related threads are launched independently and can be hard to manage or coordinate. For example, to fetch a user and order in parallel, and then process the results, you would typically use an ExecutorService
as shown below:
ExecutorService executor = Executors.newFixedThreadPool(2); Future<String> userFuture = executor.submit(() -> fetchUser()); Future<String> orderFuture = executor.submit(() -> fetchOrder()); String user = userFuture.get(); // blocks until user is fetched String order = orderFuture.get(); // blocks until order is fetched String result = process(user, order);
The downsides of the above approach are:
- If one task fails, the other continues unless manually cancelled
- The executor and tasks outlive the method unless explicitly shut down
- You must manage the executor, handle exceptions, and ensure cleanup
Structured Concurrency abstracts much of this complexity, allowing you to focus on what your code is doing rather than how to coordinate threads. It enforces a hierarchical structure, in which tasks spawned together must complete together, much like local variables within a method.
StructuredTaskScope
Here is an example of using the StructuredTaskScope
API:
try (var scope = new StructuredTaskScope<String>()) { Subtask<String> userTask = scope.fork(() -> fetchUser()); Subtask<String> orderTask = scope.fork(() -> fetchOrder()); scope.join(); // Wait for all subtasks to complete String user = userTask.get(); String order = orderTask.get(); System.out.println("user: " + user); System.out.println("order: " + order); }
StructuredTaskScope
has two subclasses, ShutdownOnSuccess
and ShutdownOnFailure
, to control how the scope reacts to task completion or failure.
StructuredTaskScope.ShutdownOnFailure
With this policy, if any task fails, the scope cancels the remaining tasks, and propagates the exception when throwIfFailed()
is called.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Subtask<String> userTask = scope.fork(() -> fetchUser()); Subtask<String> orderTask = scope.fork(() -> fetchOrder()); // wait for all subtasks to complete, or one to fail scope.join(); // throw if any subtask failed scope.throwIfFailed(); String user = userTask.get(); String order = orderTask.get(); System.out.println("user: " + user); System.out.println("order: " + order); }
StructuredTaskScope.ShutdownOnSuccess
This policy is the opposite — it stops once one task succeeds, cancelling the others. It's great when you want the first successful result and don't care about the rest.
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) { scope.fork(() -> fetchFromPrimary()); scope.fork(() -> fetchFromBackup()); // wait for any subtask to complete, or all to fail scope.join(); // get the result of the first task that completed successfully, // or throw an exception if none did System.out.println(scope.result()); }