Latest Java Reaches You!

7,109

Premium

2,149 visitors

Home » Blogs » Java » Core Java » Structured Concurrency In Java 25: A Deep Dive into Proposal

Structured Concurrency in Java 25 is open jdk the evolution of Java’s concurrency model. For developers who’ve been tracking the progress of Project Loom and its structured concurrency proposals, JEP 505 is a landmark. It finalizes and refines the Structured Concurrency API, providing Java developers with a powerful and structured way to manage parallelism—especially in tandem with virtual threads.

In this blog post, we’ll compare the structured concurrency API in Java 25 and Java 24, highlighting what’s changed, what’s improved, and why it matters. We’ll walk through realistic examples, performance insights, and use cases.

Please refer JDK Enhancement Proposal 505 revamps JEP 505 in OpenJdk: The structured concurrency in Java 25 by introducing a configuration and joiners. Most Java applications will use that API for concurrency and leveraging virtual threads from project loom, more often than we use thread pools today.


What Is Structured Concurrency?

Structured concurrency is a programming paradigm where lifetimes of concurrent subtasks are bounded by the code blocks that create them. Unlike traditional approaches where threads live independently and are difficult to manage, structured concurrency ensures all subtasks complete, fail, or get canceled in a deterministic and hierarchical manner.

  • Code clarity: Lifecycle management for all subtasks lives in one place.
  • Automatic cancellation: Failure in one subtask can cancel others.
  • Thread observability: Clear parent-child relationships show up in thread dumps.
  • Improved error handling: Exceptions can be propagated or aggregated.

Some word….

Structured  concurrency derives from a simple principle: If a task splits into concurrent subtasks,  then they all return to the same  place, namely the task’s code block.


Structured Concurrency in Java 24 Open Jdk (Preview)

Java 24 introduced structured concurrency as a preview API under the StructuredTaskScope class. While it was a good starting point, there were several limitations:

  • Subclassing was required for different shutdown behaviors (ShutdownOnFailure, ShutdownOnSuccess, etc.).
  • Results and error propagation were handled manually through throwIfFailed().
  • Custom join strategies required extending base classes.

Example: Java 24 Open Jdk StructuredTaskScope

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> result1 = scope.fork(() -> fetchDataFromServiceA());
    Future<String> result2 = scope.fork(() -> fetchDataFromServiceB());

    scope.join();
    scope.throwIfFailed(); // throws if any task failed

    System.out.println(result1.resultNow());
    System.out.println(result2.resultNow());
}

Limitations:

  • Subclassing the scope for shutdown behavior is rigid.
  • Exception handling is not integrated into the join() call.
  • Observability (e.g., thread naming) was minimal.

Structured Concurrency API in Java 25 Open Jdk: JEP 505

JEP 505 finalizes and revamps the Structured Concurrency API for Java 25. The improvements bring better ergonomics, cleaner syntax, more flexible configuration, and stronger developer guarantees.

Please refer the doc for more detail on Structured Concurrency.

Key Changes in Java 25 Open Jdk:

  • StructuredTaskScope is now a sealed interface.
  • Use StructuredTaskScope.open() with optional Config and Joiner.
  • Integrated exception propagation through join().
  • Configurable thread factories, names, and timeouts.
  • Enhanced observability with hierarchical thread dumps.

Before we move forward please take a glance at below code snippet carefully word by word and think about it while scrolling the article. Also when done take a look again at the article’s thumbnail –

   ThreadFactory factory = Thread.ofVirtual().name("javadevtech-", 0).factory();
    Duration timeout = Duration.ofSeconds(10);

    try (var scope = StructuredTaskScope.open(
    		Joiner.<String>allSuccessfulOrThrow(),
            cf -> cf.withName("Scope-Name-javadevtech")
            .withThreadFactory(factory)
            .withTimeout(timeout))) {

        scope.fork(callable1); // runs in a virtual thread with name "javadevtech-0"
        scope.fork(callable2); // runs in a virtual thread with name "javadevtech-1"

        List<String> result = scope.join()
                                   .map(Subtask::get)
                                   .toList();
   }

Just bit detail about above code snippet –

1. You get an instance by calling the static `open`  method

2. You fork subtasks by passing a `Callable`  or `Runnable` to the `fork` method,  which runs them on a new virtual thread.

3. Then you call `join` method

The Join method will TYPICALLY block the owners thread till subtasks are completed. But joiners make join method kind of unblocking by taking some responsibilities. We will see detail below.


Configuration in Java 25

Three Main Configuration Aspects:

  1. Name – for monitoring and observability.
  2. ThreadFactory – typically for creating virtual threads.
  3. Timeout – This starts when `open`  is called and, if it times out,  cancels all remaining subtasks and  throws an exception from `join`

Example: Using StructuredTaskScope.Config

StructuredTaskScope.Config config = StructuredTaskScope.Config.builder()
    .name("Scope-Name-javadevtech")
    .threadFactory(Thread.ofVirtual().name("javadevtech-", 0).factory())
    .timeout(Duration.ofSeconds(10))
    .build();

try (var scope = StructuredTaskScope.open(config)) {
    // Fork tasks here
}


Joiners: Customizing Join Behavior

Joiners in Java 25 replace rigid subclassing with composable join strategies. Joiners define how subtasks are waited on, how results are combined, and how failures are handled.

Below Is How Joiners Helps Join() Method To Be Unblocking:

  • to  react to subtask completion, be they successful  or not
  • to cancel the scope early if desired 
  • to create the exception that `join` will throw in a failure case
  • to produce a result that `join` will  return (where that is applicable).

Built-in Joiners:

There are different joiners for different use cases.

  • default Joiner (No argument in open method.) – cancel on first failure, throw exception.
  • Joiner.allSuccessfulOrThrow() – all must succeed, returns stream of results.
  • Joiner.anySuccessfulResultOrThrow() – first success wins.
  • Joiner.awaitAll() – wait for all, collect results manually.

Let’s take a look at bit detail for each of them –

Example: Default Joiner

If you are calling the parameter less variant of `open` then it is a default joiner. The default joiner works  well for subtasks with different result types that must all complete successfully.

Following is how default joiner behaves:

  • It does not react to successful completion of subtasks. 
  • It will cancel the scope if a subtask fails. 
  • And in that case, it will throw the failed subtask’s exception. 
  • In the case of all subtasks completing  successfully, no result is computed for `join` – the user is expected to get  results from the subtasks themselves.

Example: allSuccessfulOrThrow()

All subtasks return the same type and all must succeed. That means a failed subtask cancels the scope but a successful scope can return a stream of these results directly from `join`.

Example: anySuccessfulResultOrThrow()

All subtasks return the same type  but only one needs to succeed. That means as soon as the first  subtask completes successfully, the scope is canceled and the subtask’s  result is returned from `join`. Only if all subtasks fail, will  the scope itself fail as well.

Example: awaitAll()

Subtasks can return result types that are different and we want all to complete, successfully or not, before moving on. This joiner is very lazy and essentially does nothing. It’s up to the user to interrogate subtasks for their state and to get results where they are available.


Hope you may have got idea how to define the configuration and joiner. Please check the code snippet provided in the beginning as an example having both.


Important Points to Consider

OpenJdk JEP 505 has more of details to consider:

1. `join` can throw a variety of exceptions, depending on whether the scope is misused, has failed, timed out or was cancelled.

2. About misuse: The API insists on the restrictions of structured concurrency and will throw if it detects misuse, for example when code exits the scope without having called `join` or when `join` is invoked by the wrong thread.

3. Cancellation is propagated via thread interrupts, meaning primarily as `InterruptedException’ during blocking calls, both up and down the tree of nested structured task scopes. Cancelled scopes will always cancel all remaining subtasks and a cancelled subtask may cancel the scope that owns it, depending on what the joiner decides. 

4. Both structured concurrency in Java and scoped values lean on nested scopes  in a way that aligns perfectly and so subtasks  automatically inherit a task’s scoped values.

5. `jcmd` can print a thread dump,  which comes as a tree of nested scopes and includes the scope and thread names. This is a huge improvement in understanding a concurrent application’s state. 


Java 24 vs Java 25 Open Jdk: Feature Comparison

FeatureJava 24Java 25 (JEP 505)
StatusPreviewFinalized (targeted for GA)
Scope creationConstructor / Subclassing (StructuredTaskScope is a non-final call)StructuredTaskScope.open()
(StructuredTaskScope is a sealed interface)
Use casesSubclasses cover different use casesconfig/joiner cover different use cases
(If use case beyond that -> wrap a scope)
Shutdown behaviorFixed via subclassingConfigurable via Joiners
Result collectionManual + throwIfFailed()Automatic via Joiner
Timeout supportNot supportedSupported via Config
ObservabilityMinimalScope names + thread dump trees
Virtual thread integrationYesYes
API ergonomicsVerboseComposable and clean

Performance and Developer Experience

Performance:

Thanks to virtual threads, subtasks are lightweight and highly scalable. The structured concurrency model prevents resource leakage by ensuring all tasks are joined or canceled.

Developer Ergonomics:

  • No more subclassing madness
  • Clear semantics around lifecycle
  • Built-in joiners cover 90% of use cases
  • Observable structure via thread dumps

Use Case Scenarios for Structured Concurrency

1. Website Content Collection with Java

SEO tools, crawlers, and content indexers can concurrently fetch and parse multiple URLs while canceling unnecessary fetches on first success or failure. Java 25’s structured concurrency API is perfect for implementing robust content collection logic.

2. API Orchestration

Microservices that aggregate responses (e.g., pricing + inventory + shipping) benefit from scoped execution and cancellation-on-failure.

3. Parallel Data Pipelines

ETL jobs can fork stages like reading, transforming, and loading in parallel, while handling errors in a structured way.

4. File Scanning / Indexing

Scanning large directories or network shares can spawn parallel subtasks, and structured concurrency ensures all scans are completed or canceled correctly.


Conclusion

The Structured Concurrency API in Java 25 is a substantial upgrade over its Java 24 preview form. By transitioning from subclassing to configuration and composable joiners, JEP 505 significantly improves flexibility, observability, and developer experience.

Whether you’re building SEO crawlers, orchestrating microservice calls, or parallelizing data pipelines, this API—especially when combined with virtual threads—is a game-changer.

Start experimenting now with early access builds or prepare for Java 25 GA. Structured concurrency is the future of concurrent programming in Java.

Comments

Leave a Reply


Discover more from JavaDevTech platform

Subscribe to get the latest posts sent to your email.

Discover more from LEARN Java Development & Technologies

Subscribe now to keep reading and get access to the full archive.

Continue reading