Alan Bateman describes the core concept of Structured Concurrency and provides some examples at the OpenJDK Wiki.
In this blog I explain how to achieve something similar with the features and API provided by Java 14, today.
Yes, Java 8 should be fine, too.
Replace var
keyword with explicit types declarations and unroll the try-with-resource
blocks.
Ron Pressler writes in the State of Loom article
In our current prototype we represent a structured concurrency scope, the code block that confines the lifetime of child threads, by making the
java.util.concurrent.ExecutorService
anAutoCloseable
, with close shutting down the service and awaiting termination. This guarantees that all tasks submitted to the service will have terminated by the time we exit the try-with-resources block […]
Let’s take some inspiration how that “shutdown-on-close” works. Place that inspiration into a service-decorating class.
public class AutoCloseableExecutorService implements AutoCloseable, ExecutorService {
private final ExecutorService service;
public AutoCloseableExecutorService(ExecutorService service) {
this.service = service;
}
@Override
public void close() {
boolean terminated = isTerminated();
if (!terminated) {
shutdown();
// wait for termination...
}
}
@Override
public boolean isTerminated() {
return service.isTerminated();
}
@Override
public void shutdown() {
service.shutdown();
}
// remaining API from ExecutorService...
...
Next, replace Thread.builder().virtual().factory()
with:
Executors.defaultThreadFactory()
The default thread factory creates heavy-weight normal threads. That’s all we got, without Loom’s Virtual Threads.
Also, the Executors.newThreadExecutor(factory)
doesn’t exist today.
Let’s use a fixed-size thread pool executor service:
Executors.newFixedThreadPool(nThreads, factory)
Finally, here’s the example showing structured concurrency without using Loom. The test program:
System.out.println("BEGIN");
var factory = Executors.defaultThreadFactory();
try (var executor = new AutoCloseableExecutorService(Executors.newFixedThreadPool(1, factory))) {
executor.submit(this::printThreadNameAndSleep);
try (var nested = new AutoCloseableExecutorService(Executors.newFixedThreadPool(2, factory))) {
nested.submit(this::printThreadNameAndSleep);
nested.submit(this::printThreadNameAndSleep);
nested.submit(this::printThreadNameAndSleep);
}
executor.submit(this::printThreadNameAndSleep);
}
System.out.println("END.");
with this “task” method:
int printThreadNameAndSleep() throws Exception {
System.out.println(Thread.currentThread().getName());
Thread.sleep(500);
return 0;
}
yields (or slightly different, depending on which thread gets to assigned to work)
BEGIN
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-2
pool-1-thread-1
END.
So far so good.
I’m looking forward to the next project Loom updates and seeing it in the Java main line, soon! In the meantime, test-driving Loom with https://github.com/sormuras/junit5-looming … 1.000.000 tests in parallel.