DEV Community

Sadiul Hakim
Sadiul Hakim

Posted on

Java 25 Structured Concurrency (Preview) — The Complete Guide

1. What Is Structured Concurrency?

The Problem (Before Java 21)

Traditionally, Java concurrency used tools like:

  • Thread, ExecutorService, Future, and CompletableFuture.
  • You’d manually create threads, track them, and handle cancellations or exceptions yourself.

The problem?
It’s hard to manage lifecycles. Threads could outlive their parent logic.
Exceptions might get lost, and cancellation is messy.

Example:

Future<String> f1 = executor.submit(this::task1);
Future<String> f2 = executor.submit(this::task2);

String result1 = f1.get(); // blocks
String result2 = f2.get();
Enter fullscreen mode Exit fullscreen mode

If one fails, the other still runs. You must remember to cancel or handle it.
That’s unstructured concurrency.


The Solution: Structured Concurrency (JEP 453 / Java 21+)

Structured concurrency treats concurrent subtasks as structured blocks,
just like try-with-resources or nested method calls.

  • All threads are started, joined, and canceled together.
  • The parent waits for all subtasks to finish.
  • Exceptions are automatically aggregated and propagated.

Think of it as:

“Concurrency with clean lifecycles — all children finish before the parent exits.”


2. Key Concepts

Concept Description
StructuredTaskScope A scope that manages subtasks together. It starts, joins, cancels, and aggregates results.
Subtask A unit of work submitted to the scope via fork().
Joiner Defines how the scope joins results — for example, stop after any success, all success, or a custom condition.
Structured Concurrency The programming model — ensures that concurrent tasks are managed as a structured whole.
Virtual Threads Lightweight threads from Project Loom — perfect for structured concurrency since they scale to millions of tasks efficiently.

3. StructuredTaskScope in Action

The class StructuredTaskScope manages subtasks:

try (var scope = StructuredTaskScope.open()) {
    var t1 = scope.fork(() -> task("A"));
    var t2 = scope.fork(() -> task("B"));

    scope.join(); // Wait for all subtasks to finish
    System.out.println(t1.get());
    System.out.println(t2.get());
}
Enter fullscreen mode Exit fullscreen mode

When you exit the try block:

  • All threads are joined.
  • If any exception occurred, it’s handled properly.
  • The scope cleans itself up.

4. Built-in Joiners

allSuccessfulOrThrow()

Waits for all subtasks to succeed or throws an exception.

try (var scope = StructuredTaskScope.open(allSuccessfulOrThrow())) {
    var t1 = scope.fork(() -> task(2, false));
    var t2 = scope.fork(() -> task(3, true)); // this fails
    scope.join();
}
Enter fullscreen mode Exit fullscreen mode

If any task fails → it throws an exception after joining.


anySuccessfulResultOrThrow()

Waits for the first successful task, cancels the rest.

try (var scope = StructuredTaskScope.open(anySuccessfulResultOrThrow())) {
    scope.fork(() -> task(4, false));
    scope.fork(() -> task(2, true));
    scope.fork(() -> task(3, false));

    Object result = scope.join();
    System.out.println(result);
}
Enter fullscreen mode Exit fullscreen mode

As soon as one subtask returns successfully, others are canceled.


Custom Joiner (like TargetSuccessJoiner)

You can define your own joining logic using StructuredTaskScope.Joiner.


5. Custom Joiner: TargetSuccessJoiner

Let’s understand your custom joiner step-by-step:

static class TargetSuccessJoiner<T> implements StructuredTaskScope.Joiner<T, List<T>> {
    private final int targetCount;
    private final List<T> successfulResults = Collections.synchronizedList(new ArrayList<>());
    private final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<>());

    TargetSuccessJoiner(int targetCount) {
        this.targetCount = targetCount;
    }
Enter fullscreen mode Exit fullscreen mode

targetCount = how many successful results you want before stopping.
Example: You may only need 2 successful responses from 10 servers.


The onComplete() Method

Called when each subtask finishes:

@Override
public boolean onComplete(StructuredTaskScope.Subtask<? extends T> subtask) {
    switch (subtask.state()) {
        case SUCCESS -> {
            System.out.println("Success: " + subtask.get());
            successfulResults.add(subtask.get());
            if (successfulResults.size() >= targetCount) {
                System.out.println("🚦 Reached " + targetCount + " successes — shutting down scope!");
                return true; // stop scope early
            }
        }
        case FAILED -> {
            System.out.println("Failure: " + subtask.exception());
            exceptions.add(subtask.exception());
        }
    }
    return false; // continue waiting
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Each subtask completion is observed.
  • When enough successes are reached, it ends early.
  • Failed subtasks are recorded.

The result() Method

Called after all tasks have completed or stopped early.

@Override
public List<T> result() throws Throwable {
    if (successfulResults.size() >= targetCount) {
        return List.copyOf(successfulResults);
    } else {
        Exception ex = new Exception("Fewer than " + targetCount + " tasks succeeded");
        exceptions.forEach(ex::addSuppressed);
        throw ex;
    }
}
Enter fullscreen mode Exit fullscreen mode

If not enough successes, it throws an exception with all suppressed errors attached.


6. Full Example: twoSuccessfulJoiner()

private static void twoSuccessfulJoiner() {
    try (var scope = StructuredTaskScope.open(new TargetSuccessJoiner<String>(2))) {
        scope.fork(() -> task(4, false));
        scope.fork(() -> task(3, false));
        scope.fork(() -> task(2, true));
        scope.fork(() -> task(5, false));

        List<String> results = scope.join();
        System.out.println("Collected: " + results);
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}
Enter fullscreen mode Exit fullscreen mode

Behavior:

  • Four tasks run concurrently.
  • Stop after 2 successes.
  • Failures are logged but do not prevent others from running.
  • join() returns a list of successful results.

7. Example Task Method

private static String task(int sec, boolean fail) {
    try {
        TimeUnit.SECONDS.sleep(sec);

        if (fail) throw new RuntimeException("Task Failed");
        return String.format("Task finished in %s sec", sec);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
Enter fullscreen mode Exit fullscreen mode

This simulates a slow task — either success or failure after a delay.


8. With Timeout Example

try (var scope = StructuredTaskScope.open(allSuccessfulOrThrow(),
        cf -> cf.withTimeout(Duration.ofSeconds(4)))) {
    var t1 = scope.fork(() -> task(4, false));
    var t2 = scope.fork(() -> task(5, false));
    scope.join();
}
Enter fullscreen mode Exit fullscreen mode

If subtasks don’t finish in time, the scope cancels them automatically.


9. Structured Concurrency + Virtual Threads

Structured Concurrency works beautifully with Virtual Threads (Project Loom).

Virtual Threads Recap:

  • Introduced in Java 21.
  • Extremely lightweight — you can spawn millions.
  • Perfect for I/O-bound concurrent tasks.

Example:

try (var scope = StructuredTaskScope.open()) {
    var task1 = scope.fork(() -> Thread.ofVirtual().factory().newThread(() -> task(2, false)));
    var task2 = scope.fork(() -> task(3, false));
    scope.join();
}
Enter fullscreen mode Exit fullscreen mode

In fact, StructuredTaskScope automatically uses virtual threads by default (in most modern JDK versions).

So you get massive scalability + clean structure.


10. Why Use Structured Concurrency?

Benefit Description
Simplicity One scope manages all child tasks.
Safety No thread leaks, no forgotten joins.
Propagated Exceptions All exceptions bubble up cleanly.
Cancellable You can stop early (like with anySuccessfulResultOrThrow or custom logic).
Works with Virtual Threads Efficient and scalable.

Final Thoughts

Structured concurrency brings clarity and safety to concurrent Java code:

  • You can now treat parallel work as structured blocks.
  • It’s composable, maintainable, and easy to reason about.

Quick Summary

Concept Purpose
StructuredTaskScope Parent scope for concurrent subtasks
Subtask Individual child computation
Joiner Strategy for combining or short-circuiting results
Virtual Threads Lightweight concurrency engine
Custom Joiner Define advanced success/failure logic

Top comments (0)