Non-obvious behavior in newVirtualThreadPerTaskExecutor

Stig Rohde Døssing stigdoessing at gmail.com
Mon Dec 1 20:16:23 UTC 2025


Hi,

I stumbled on a minor landmine with newVirtualThreadPerTaskExecutor, and
figured I'd share in case something can be done to communicate this
behavior a little better, because it caught me by surprise.

JEP 444 suggests code like the following

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        var future1 = executor.submit(() -> fetchURL(url1));
        var future2 = executor.submit(() -> fetchURL(url2));
        response.send(future1.get() + future2.get());
} catch (ExecutionException | InterruptedException e) {
        response.fail(e);
}

This led me to believe that I could write code like that, and get something
that reacts to interrupts promptly. But unfortunately, the close method on
this executor does not interrupt the two submitted fetches. Instead, if the
main thread in this code is interrupted, the thread will block until the
two fetches complete on their own time, and then it will fail the
request, which seems a little silly.

Due to how try-with-resources works, the executor is closed before the
catch block is hit, so in order to "stop early" when interrupted, you'd
need to add a nested try-catch inside the try-with-resources to either
interrupt the two futures or call shutdownNow on the executor when the main
thread is interrupted.

I know that the structured concurrency API will be a better fit for this
kind of thing, but given that virtual threads are likely to spend a lot of
their time blocking waiting for something to occur, it seems a little
unfortunate that the ThreadPerTaskExecutor for virtual threads doesn't do
shutdownNow on close when used in the most straightforward way.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20251201/a88c8cb2/attachment.htm>


More information about the loom-dev mailing list