Contents

Grouping virtual threads

Written by: David Vlijmincx

Introduction

This post looks at how to combine multiple threads and return the results when all the threads are done running. You can use this for example to do multiple requests and wait for all the threads in an easy way.

Read my article about virtual threads if you want to know more about them, and if you want to know more structured concurrency you should read this article.

Grouping threads using the ExecutorService

The easiest way is to use the ExecutorService. The ExecutorService now implements the autocloseable interface which means that we can use it inside a try-with-resource statement. In the following example, we create an ExecutorService inside a try-with-resource statement. The benefit of doing this is that we only exit the try-with-resource statement when all the submitted tasks are done.

1
2
3
4
5
6
7
Runnable printTask = () -> System.out.println("Hello, World!");

try(ExecutorService e = Executors.newVirtualThreadPerTaskExecutor()){
    e.submit(printTask);
    e.submit(printTask);
    e.submit(printTask);
}

In the previous example, we submit three printTasks to the ExecutorService and we exit the try when all three tasks have printed their message to the output.

Grouping threads using structured concurrency

Before you attempt this, structured concurrency is still an incubating API and not in a mainstream version of Java yet.

You can also use structured concurrency to group threads, which is a really powerful way of doing so. With structured concurrency, we create a scope and inside that scope, we spawn threads that will belong to that scope. With structured concurrency, you can use the scope as a manager of the virtual thread. You can use the ShutdownOnFailure shutdown policy to create a scope for your virtual threads.

In the following example, we create a ShutdownOnFailure scope inside a try-with-resource statement. The ShutdownOnFailure shutdown policy states that either all threads finish, or none of them do. So all the threads finish successfully or if one throws an exception the still-running threads will be stopped.

What happens inside the try-with-resource statement is we create virtual threads using the scope.fork() method. When we have created all the threads we needed we call the scope.join(). This will block the parent thread till all the threads are done or stopped because of an exception. To check if an exception has happened we use the scope.throwIfFailed() method which will rethrow the exception if one occurs. Before we exit the try-with-resource statement in the example, we build the return object for the caller.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

    Future<String> futureA = scope.fork(() -> "result1");
    Future<String> futureB = scope.fork(() -> "result2");
    
    scope.join();
    
    scope.throwIfFailed();
    
    return "result: " + futureA.resultNow() + " " + futureB.resultNow();
}

If you need a StructuredTaskScope that fits your use case specifically you can also create your own StructuredTaskScope. To learn how to do that please read this post about it.

Conclusion

With virtual threads, you have two powerful ways to manage threads as a group. The most straightforward way is to use the ExecutorService which now implements the autocloseable interface. If you need a more powerful way to manage threads you can use the StructuredTaskScope which it's built-in shutdown policies or create your own policy.

Further reading