Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’ll discuss the incubator feature Structured Concurrency (JEP 428), which provides structured concurrency capabilities to Java 19. We’ll guide you through the usage of the new APIs for managing multithreaded code.

2. Idea

Enhance the maintainability, reliability, and observability of multithreaded code by adopting a concurrent programming style that reduces the likelihood of thread leaks and cancellation delays, which are common risks associated with cancellation and shutdown. To better understand the problems of unstructured concurrency, let’s have a look at an example:

Future<Shelter> shelter;
Future<List<Dog>> dogs;
try (ExecutorService executorService = Executors.newFixedThreadPool(3)) {
    shelter = executorService.submit(this::getShelter);
    dogs = executorService.submit(this::getDogs);
    Shelter theShelter = shelter.get();   // Join the shelter
    List<Dog> theDogs = dogs.get();  // Join the dogs
    Response response = new Response(theShelter, theDogs);
} catch (ExecutionException | InterruptedException e) {
    throw new RuntimeException(e);
}

While getShelter() is running, the code won’t notice if getDogs() possibly fails and will continue unnecessarily cause of the blocking shelter.get() call. As a result, only after getShelter() finishes and getDogs() returns, dogs.get() will throw an exception, and our code will fail:

code fail

But this isn’t the only problem. When the thread executing our code gets interrupted, it’ll not propagate the interruption to our subtasks. Additionally, if the first executed subtask shelter throws an exception, it’ll not be delegated to the dogs’ subtask, and it’ll keep running, wasting resources.

Structured concurrency attempts to address these issues, as we’ll see in the next chapter.

3. Example

For our structured concurrency example, we’ll use the following records:

record Shelter(String name) { }

record Dog(String name) { }

record Response(Shelter shelter, List<Dog> dogs) { }

We’ll also provide two methods. One to get a Shelter:

private Shelter getShelter() {
    return new Shelter("Shelter");
}

The other is to retrieve a list of Dog elements:

private List<Dog> getDogs() {
    return List.of(new Dog("Buddy"), new Dog("Simba"));
}

Since structured concurrency is an incubator feature, we have to run our application with the following parameters:

--enable-preview --add-modules jdk.incubator.foreign

Otherwise, we could add a module-info.java and mark the package as required.

Let’s check out an example:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<Shelter> shelter = scope.fork(this::getShelter);
    Future<List<Dog>> dogs = scope.fork(this::getDogs);
    scope.join();
    Response response = new Response(shelter.resultNow(), dogs.resultNow());
    // ...
}

Since StructuredTaskScope implements the AutoCloseable interface, we can use it within a try-with-resources statement. The StructuredTaskScope offers us two subclasses, which have different uses. In this tutorial, we’ll use ShutdownOnFailure(), which shuts down subtasks in case something goes wrong.

There is also a ShutdownOnSuccess() constructor, which does the opposite. It shuts down the subtasks in case of success. This short-circuiting pattern helps us to avoid unnecessary work.

The use of StructuredTaskScope strongly resembles the structure of synchronous code. The thread that creates the scope is the owner. The scope allows us to fork further subtasks in the scope. This code is called asynchronously. With the help of the join() method, we can block all tasks until they have delivered their result.

Each task can terminate the other tasks with the help of the shutdown() method of the scope. The throwIfFailed() method offers another possibility:

scope.throwIfFailed(e -> new RuntimeException("ERROR_MESSAGE"));

It allows us to propagate any exception if any fork fails. Besides, we can also set a deadline with joinUntil:

scope.joinUntil(Instant.now().plusSeconds(1));

This will throw an exception after the time has expired if the tasks didn’t finish yet.

4. Conclusion

In this article, we discussed the drawbacks of unstructured concurrency and how structured concurrency attempts to address these issues. We learned how to handle errors and implement deadlines. We also have seen how the new construction makes it easier to write maintainable, readable, and reliable multithreaded code synchronously.

As always, these examples can also be found over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!