top of page
  • khangaonkar

JAVA Structured Concurrency Tutorial

Overview

In the previous blog on Virtual threads, I talked about how virtual threads make it easier to build highly concurrent application with a large number of threads. Structured concurrency makes it easier to make such applications reliable.


Structured concurrency is based on the thinking that there is a natural relationship between tasks and subtasks. This relationship should be preserved for code to be maintainable, reliable, readable and error free.


structured concurrency scope of tasks
Structured concurrency

Structured concurrency is available as a preview feature in JDK 21.


By the end of this blog, you will understand what structured concurrency is and how to use it to code programs that are maintainable, reliable and debuggable.


What is Structured Concurrency ?

The term "structured concurrency" sounds intimidating. In reality it is quite easy to understand.


It is a new API in the java.util.concurrency package to eliminate the leakage and unpredictability of threads when unexpected cancellation or shutdown or errors happen.


This statement from JEP 453 very clearly sums up the feature:

If a task splits into concurrent subtasks then they all return to the same place, namely the task's code block.


When you start a thread in the old way, once started, the thread is on its own. It never returns to the code that started it.


Structured concurrency lets you manage related subtasks (virtual threads) as single unit.

A task (thread or virtual thread) can

  • create any number of subtasks

  • join them or wait for them to be completed

  • cancel them of necessary

  • handle errors easily


The key classes in this API are


This is a preview API java. You need to enable it with the --enable-preview command line option.


Why Structured concurrency ?

The code below shows the issue with unstructured concurrency.

ExecutorService es = Executors.newCachedThreadPool();
List<Result> doMultipleTasks() throws ExecutionException, InterruptedException {
    Future<Result>  work1  = es.submit(() -> work1());
    Future<Result> work2 = es.submit(() -> work2());
    List<Result> ret = new ArrayList(); 
    ret.add(work1.get());  
    ret.add(work2.get());
    return ret;
}

Some issues with the above code are:

  • if either work1 or work2 fails, the other will continue running, which we might not want.

  • if the thread running doMultipleTasks fails, the subtasks will keep running

  • if work2 completes or fails early, while work1 takes time, until work1.get() returns we are blocked

  • The biggest flaw in the above example is that, while the code implies a relationship between the thread running doMultipleTask and the threads that will run work1 and work, in runtime, in the JVM, when the code executes, there is no relationship.


Let us rewrite the above code using structured concurrency.

List<Result> doMultipleTasks() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope<Result>.ShutdownOnFailure()) {
        SubTask<Result>  work1  = scope.fork(() -> work1());
        SubTask<Result> work2 = scope.fork(() -> work2);

        scope.join()            // wait until complete
             .throwIfFailed();  // error case

        // all tasks success
        List<Result> ret = new ArrayList(); 
       ret.add(work1.get());  
       ret.add(work2.get());
       return ret;
    }
}

The benefits of the above code are:

  • If either work1 or work2 fails, the other is cancelled.

  • If the calling thread fails or is cancelled, both work1 and work2 are cancelled.

The execution in runtime mirrors the parent child relationship implied by the code. A thread dump will show the hierarchy.


How to use Structured concurrency?

Step 1: Create a scope

try (var scope = new StructuredTaskScope<Result>()) {

}

Step 2: Within the scope, create subtasks

try (var scope = new StructuredTaskScope<Result>()) {
	SubTask<Result>  work1  = scope.fork(() -> work1());
     SubTask<Result> work2 = scope.fork(() -> work2);

}

A virtual thread is created for each subtask.

Step 3: Make the calling thread wait for all the subtasks to complete (succeed or fail) by calling join() on the scope.

try (var scope = new StructuredTaskScope<Result>()) {
	SubTask<Result>  work1  = scope.fork(() -> work1());
     SubTask<Result> work2 = scope.fork(() -> work2);

	scope.join();
}

Step 4: Get and return the results

Result result1 = work1.get();
Result result2 = work2.get();

Step 5: Close the scope. Since we have enclosed the scope in a try {} block, that is done implicitly.

Scope shutdown policies

Without more guidance, the join() method will wait until every task either completes or fails.


However using StructuredTaskScope.ShutdownOnFailure() will create a scope that will shutdown all tasks as soon as one them fails.

And using StructuredTaskScope.ShutdownOnSuccess() will create a scope that will shutdown all tasks as soon as one of them succeeds.


The join() method waits indefinitely until the shutdown policy is met. But you can impose a deadline and force shutdown of all tasks using the joinUntil(deadline) method and passing it an java.time.Instant.


The ShutdownOnFailure scope has a throwIfFailed method you can call to throw the exception if one of the subtasks fails.

scope.join().throwIfFailed(); 

Results

When there are results from multiple SubTasks, you can get the result for each subtask by calling the get() method on each subtask.

However in the case of ShutdownOnSuccess , we are interested in only the first result and you get that by calling getresult() on the ShutdownOnSuccess scope.


Other Notes

This feature does not replace Executors, ExecutorService, Future and other unstructured concurrency features. They have a place in some scenarios. But for most cases, structured concurrency makes the programmers life easier. The usual caveats about concurrent programming such as visibility, race conditions, deadlocks etc all apply.


Summary

The term "structured concurrency" sounds intimidating. But it is just a simple, yet big improvement in JAVA that makes it easy to write highly concurrent application that are maintainable and reliable. Virtual threads let you create millions of subtasks at a low cost. Structured concurrency ensures the your code behaves as desired.





Recent Posts

See All

Go review: Should I use the Go programming language ?

Overview Go was developed by engineers at Google. Their primary motivation was dislike for C++. Goal was to create a language for systems programming. Its popularity has been slowly but steadily incre

JAVA Record tutorial

Java record type was introduced as a preview in JDK 14 and made official in JDK 16. It is a simple feature but a really important one that should be used more often. This is short tutorial on the reco

bottom of page