Structured Concurrency in Java, finally

Lavneesh
5 min readApr 10, 2023

--

Structured concurrency(JEP-428) is a programming approach that organizes concurrent tasks hierarchically, requiring parent tasks to wait for their child tasks to complete before finishing. This method minimizes resource leaks, avoids orphaned tasks, and makes concurrent code easier to manage. Structured concurrency uses the light weight VirtualThread implementation provided by project Loom.

Structured Task Scope

StructuredTaskScope is a key component of structured concurrency. It provides a mechanism to manage the lifecycle of virtual threads and ensure proper nesting of concurrent tasks. When using StructuredTaskScope, the parent task will wait for all child tasks to complete before finishing. It’s a way of working with child tasks as if they were a single task instead of multiple tasks.

Consider a scenario where you are developing a weather application that utilizes multiple weather service providers. To enhance the user experience, your application retrieves weather data from all providers and promptly returns the result as soon as one provider responds. In this context, the child tasks (weather provider calls) can be collectively treated as a single unit, and the final outcome of the operation can be derived from any of the responses. Let’s examine the implementation in code.

//Weather service interface
interface WeatherService {
String getWeather();
}

//Weather provider 1
class WeatherService1 implements WeatherService {

@Override
public String getWeather() {
waitRandom(); //Wait for random amount of time
return "Sunny";
}
}

//Weather provider 2
class WeatherService2 implements WeatherService {

@Override
public String getWeather() {
waitRandom();//Wait for random amount of time
return "Rainy";
}
}

//Weather provider 3
class WeatherService3 implements WeatherService {

@Override
public String getWeather() {
waitRandom();//Wait for random amount of time
return "Cloudy";
}
}

In the preceding code, we have established a WeatherService interface and furnished three distinct, straightforward implementations to emulate three separate weather providers. Next, we will explore how to utilize StructuredTaskScope to develop the weather fetch service while adhering to the principles of structured concurrency.

public static void main(String[] args) {
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
//A new task scope is opened
Future<String> res1 = scope.fork(() -> new WeatherService1().getWeather());
Future<String> res2 = scope.fork(() -> new WeatherService2().getWeather());
Future<String> res3 = scope.fork(() -> new WeatherService3().getWeather());
scope.join();
System.out.println(scope.result());
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

The StructuredTaskScope class offers two static inner classes: ShutdownOnSuccess and ShutdownOnFailure, both of which extend the StructuredTaskScope class. To implement structured concurrency, we can instantiate either of these classes depending on our needs or create a custom class extending the StructuredTaskScope. After initialization, tasks can be submitted to the StructuredTaskScope instance using the fork method. Internally, StructuredTaskScope leverages VirtualThreads provided by Project Loom. With each new task forked, a new VirtualThread is created.

ShutdownOnSuccess captures the first result and shuts down the task scope to interrupt unfinished threads and wakeup the owner. This class is intended for cases where the result of any subtask will do (“invoke any”) and where there is no need to wait for results of other unfinished tasks. It defines methods to get the first result or throw an exception if all subtasks fail.

ShutdownOnFailure captures the first exception and shuts down the task scope. This class is intended for cases where the results of all subtasks are required (“invoke all”); if any subtask fails then the results of other unfinished subtasks are no longer needed. If defines methods to throw an exception if any of the subtasks fail

Since, the StructuredTaskScope class implements AutoCloseable, it is recommended to open a scope in a try-with-resources block.

Task scopes can be opened from inside other tasks scope and this leads to a tree like structure where the enclosing tasks scope becomes the parent of newly created task scope. This leads to a tree like structure which is useful when passing scoped values (JEP-429).

Task scopes form a tree where parent-child relations are established implicitly when opening a new task scope.

- A parent-child relation is established when a thread started in a task scope opens its own task scope. A thread started in task scope “A” that opens task scope “B” establishes a parent-child relation where task scope “A” is the parent of task scope “B”.
- A parent-child relation is established with nesting when a thread opens task scope “B”, then opens task scope “C” (before it closes “B”), then the enclosing task scope “B” is the parent of the nested task scope “C”.
The descendants of a task scope are the child task scopes that it is a parent of, plus the descendants of the child task scopes, recursively

How is it different from ExecutorService, ThreadPools, CompletableFuture etc

Structured concurrency and Java’s existing concurrency mechanisms both aim to manage concurrent tasks, but they do so in different ways and have distinct advantages and disadvantages.

Structured Concurrency

  1. Hierarchical organization: Structured concurrency enforces parent-child relationships between tasks, requiring parent tasks to wait for child tasks to complete before finishing.
  2. Reduced resource leaks: By ensuring that parent tasks wait for child tasks, structured concurrency minimizes resource leaks, as all resources are cleaned up when the entire task hierarchy completes.
  3. Prevention of orphaned tasks: Structured concurrency avoids orphaned tasks, as child tasks must complete before their parents, preventing tasks from being left unattended.
  4. Easier error handling: Structured concurrency can make error handling and propagation more straightforward, as errors can be handled and propagated within the task hierarchy.

ExecutorService and Thread Pools

  1. ExecutorService and Thread Pools: Java’s concurrency framework provides high-level APIs like ExecutorService and various thread pool implementations, such as the FixedThreadPool and the CachedThreadPool, to manage concurrent tasks.
  2. CompletableFuture: Java’s CompletableFuture class enables asynchronous computation, allowing developers to chain and combine tasks in a more structured manner. However, this doesn’t inherently enforce structured concurrency.
  3. Less organized: Java’s concurrency mechanisms don’t inherently enforce structured concurrency, making it challenging to reason about and manage concurrent code in some cases.

Conclusion

In conclusion, structured concurrency offers a paradigm shift in Java’s approach to concurrent programming by providing an organized, hierarchical, and reliable way to manage concurrent tasks. It introduces parent-child relationships between tasks and ensures that parent tasks don’t complete until all their child tasks have finished. With Project Loom, Java is set to revolutionize concurrent programming by incorporating lightweight concurrency primitives like virtual threads and structured concurrency concepts.

The use of StructuredTaskScope and the related API changes make it easier for developers to write clean, maintainable, and efficient concurrent code. By simplifying error handling and propagation, reducing the chances of resource leaks, and preventing orphaned tasks, structured concurrency paves the way for a more robust and scalable concurrent programming experience.

As Java continues to evolve, embracing structured concurrency and exploring the advancements brought by Project Loom will be critical for developers looking to stay ahead in the world of concurrent programming. By adopting structured concurrency, Java developers can unlock the full potential of modern multicore processors and write more efficient, scalable, and maintainable concurrent applications.

--

--