Structured Concurrency in Java 21 Explained with Examples

Structured Concurrency in Java 21 is a powerful feature, designed to simplify concurrent programming and make it safer. If you’re new to this concept, don’t worry β€” this guide breaks it down in simple terms with full examples!

Structured Concurrency in Java 21 Explained with Examples

πŸš€ What is Structured Concurrency in Java 21?

Traditionally, managing multiple threads in Java has been tricky β€” you start threads, forget to stop them, or struggle with error handling. Structured Concurrency changes that by treating multiple tasks as a single unit of work.

Think of it like this:

β€œIf one task fails, cancel the others. If all succeed, collect the results.”

It brings a more organized, readable, and safer way to write concurrent code.

🧰 New API: StructuredTaskScope

StructuredTaskScope is a class introduced in Java 21 as part of the Structured Concurrency API (in java.util.concurrent). It provides a clean and organized way to manage concurrent tasks that are logically related and need to be started, waited upon, and cancelled together.

1. Create a subclass or use StructuredTaskScope directly

You can either:

  • Use StructuredTaskScope as is
  • Or extend it for specific behaviors (like handling failure)

try (var scope = new StructuredTaskScope()) {
    // your logic here
}

2. Fork tasks using fork()

Use fork() to start subtasks (concurrent threads), returning a Subtask handle.


Subtask task1 = scope.fork(() -> fetchDataFromServiceA());
Subtask task2 = scope.fork(() -> fetchDataFromServiceB());

3. Join all tasks using join()

Wait for all tasks to complete (either normally, exceptionally, or by cancellation).


scope.join();

4. Handle task completion status using throwIfFailed()

After joining, check if any task failed, and throw the exception if needed.


scope.throwIfFailed();

5. Get results from the tasks

Retrieve results from each Subtask using .get().


String resultA = task1.get();
String resultB = task2.get();

6. Auto-close with try-with-resources

StructuredTaskScope implements AutoCloseable, so it ensures proper cleanup when used in a try block.


try (var scope = new StructuredTaskScope<>()) {
    // tasks
}

🎯 Benefits of StructuredTaskScope

  • Scoped lifetimes: Tasks live and die with the scope
  • Error propagation: If one task fails, the whole operation can be aborted
  • Cancellation support
  • Easier debugging and resource management

βœ… Basic Example: Running Two Tasks Concurrently

Let’s write a Java program that runs two tasks concurrently using Structured Concurrency.

πŸ“ File: StructuredExample.java

Package: com.kscodes


package com.kscodes;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;
public class StructuredExample {
    public static void main(String[] args) {
        try {
            String result = fetchUserProfileAndSettings();
            System.out.println("Final Result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
    public static String fetchUserProfileAndSettings() throws InterruptedException, ExecutionException {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            StructuredTaskScope.Subtask userProfileTask = scope.fork(() -> {
                Thread.sleep(500); // Simulate delay
                return "UserProfile";
            });
            StructuredTaskScope.Subtask userSettingsTask = scope.fork(() -> {
                Thread.sleep(1000); // Simulate longer delay
                return "UserSettings";
            });
            scope.join()                // Wait for all tasks
                 .throwIfFailed();     // Throw if any failed
            String profile = userProfileTask.get();
            String settings = userSettingsTask.get();
            return profile + " + " + settings;
        }
    }
}

🧠 How This Works

  1. StructuredTaskScope.ShutdownOnFailure ensures all tasks are canceled if one fails.
  2. scope.fork(...) runs a task.
  3. scope.join() waits for all tasks to complete.
  4. scope.throwIfFailed() throws an exception if any task failed.
  5. task.get() retrieves the result.

❌ What if One Task Fails?

Here’s an example that shows failure handling.

πŸ“ File: StructuredFailureExample.java



package com.kscodes;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;

public class StructuredFailureExample {

    public static void main(String[] args) {
        try {
            runTasks();
        } catch (Exception e) {
            System.out.println("Caught Exception: " + e.getMessage());
        }
    }

    public static void runTasks() throws InterruptedException, ExecutionException {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

            scope.fork(() -> {
                Thread.sleep(300);
                return "Success";
            });

            scope.fork(() -> {
                Thread.sleep(100);
                throw new RuntimeException("Something went wrong!");
            });

            scope.join().throwIfFailed();
        }
    }
}

🧾 Output:


Caught Exception: java.lang.RuntimeException: Something went wrong!

As soon as one task fails, the other is automatically canceled β€” keeping your program safe and efficient.

πŸ“Œ Benefits of Structured Concurrency

  • βœ… Easier to understand concurrent code
  • βœ… Safer: no forgotten threads
  • βœ… Built-in error handling
  • βœ… Ideal for working with Virtual Threads (Java 21 feature)

πŸ’‘ Final Thoughts

Structured Concurrency in Java 21 is a game-changer. It brings clarity, safety, and simplicity to concurrent programming. Whether you’re building web services or background processing, this new approach helps you write better code β€” faster.

Reference : Details of Structured Concurrency from Java docs