Nugget Friday - Structured Concurrency in Java

Photo of Luqman Saeed by Luqman Saeed

Welcome to another episode of Nugget Friday! Today, we'll be looking at asynchronous operations, which have always been a crucial component for creating efficient and responsive applications. 

The Problem

Java has long had support for asynchronous programming, but it hasn't always been a smooth ride. Managing thread lifecycles with ExecutorService, juggling callbacks and writing code that's both readable and testable have been ongoing challenges. It's easy to fall into the trap of overusing asynchronous-based frameworks, writing everything as asynchronous code, even when it's not necessary, just to keep things consistent.

The Solution

Introduced as an incubator project and now in preview in JDK 22, structured concurrency offers a cleaner, more organized way to manage concurrent tasks. At its core is the StructuredTaskScope, a powerful tool for launching and managing virtual threads.

Introducing StructuredTaskScope

Imagine you're fetching data from a web server. With StructuredTaskScope, you can create a "scope" to control the lifetime of your virtual threads. You can fork multiple threads (subtasks) within this scope, each performing its own operation, and wait for them all to complete before moving on. It's like having a neat little package for your concurrent work.

 

StructuredTaskScope.Subtask<String> subtask = scope.fork(() -> fetchDataFromServer());
StructuredTaskScope.Subtask<String> subtask2 = scope.fork(() -> fetchDataFromExternalService());

// ... other subtasks
scope.join(); // Wait for all subtasks to complete
//Combine the results from all subtasks...
return Stream.of(subtask, subtask2).map(StructuredTaskScope.Subtask::get)
.collect(Collectors.joining(", ", " { ", " } "));

 

In the above code snippet, we have two subtasks - one fetches data from our own server and another goes out to an external service to fetch data. These two together form the complete response that we need. Using StructuredTaskScope, we are able to break the main task, which is fetch the needed data from both services a structure and scope within a given block.

As StructuredTaskScope is just a thin wrapper around virtual threads, which in turn are cheap to spawn and discard, using the try catch block will close and make the scope available for garbage collection. The subtasks will get spawned to individual virtual threads, each carrying out their task independently. The final line in the above snippet returns the result by joining the result from all the subtasks into a single JSON string.

ExecutorService vs. StructuredTaskScope

While ExecutorService focuses on managing platform threads, StructuredTaskScope is designed for the lightweight virtual threads. You don't need to worry about pooling or reusing them – use them and discard them. This makes StructuredTaskScope ideal for tasks that involve lots of blocking and unblocking operations.

Advanced StructuredTaskScope Features

Java provides specialized versions of StructuredTaskScope for specific scenarios:

  • ShutdownOnSuccess: Returns the result of the first successful task and cancels the rest. It is perfect for situations where you need just one result out of multiple possibilities. We will look at this in a future Nugget Friday.
  • ShutdownOnFailure: Returns the exception of the first failed task and cancels the rest. It is useful when you need to know if any task fails and stops everything else. We will also look at this in a future Nugget Friday.

Conclusions

Structured concurrency, with its StructuredTaskScope, brings a new level of clarity and organization to asynchronous programming in Java. It helps you write code that's easier to read, debug, test and maintain. So, next time you're tackling a concurrent task, give structured concurrency a try – it might just be the missing piece you've been looking for!

That's it for this Nugget Friday! Happy coding!

 

Related Posts

Comments