Non-obvious behavior in newVirtualThreadPerTaskExecutor
Stig Rohde Døssing
stigdoessing at gmail.com
Mon Dec 1 22:38:20 UTC 2025
Hi Viktor,
You are right, the documentation already outlines this behavior, and
reading the try-with-resources specification makes the rest clear. It is
just an unfortunate example in JEP 444.
If the main thread is only sent a single interrupt, then by the time
ExecutorService.close is called, the main thread will no longer be
interrupted, since that flag is cleared when the InterruptedException is
thrown.
This means that if you send the main thread a single interrupt, the code
ends up blocking for just as long as it would have otherwise, the only
effect the interrupt has is to cause the request to fail, but that failure
happens "late" only after the two fetches have completed normally. That's
not the kind of behavior I think you'd want from this kind of code, you
likely would want those two fetches to be cancelled, so it was surprising
to me.
The only way to get this code to actually stop early is to have an external
source send repeated interrupts to the main thread, such that the main
thread will be interrupted twice: Once to leave future.get, and once to
cause ExecutorService.close to interrupt the tasks.
But I think it's just a slightly unclear example, and it's possible to work
around by creating a wrapping ExecutorService that invokes shutdownNow on
close.
Thanks for your answer, and you are right, we likely want to use SC instead
of this executor.
Den man. 1. dec. 2025 kl. 22.19 skrev Viktor Klang <viktor.klang at oracle.com
>:
> Hi Stig,
>
> *Executors.newVirtualThreadPerTaskExecutor()*'s Javadoc states:
>
>
> *"Creates an Executor that starts a new virtual Thread for each task. The
> number of threads created by the Executor is unbounded. This method is
> equivalent to invoking newThreadPerTaskExecutor(ThreadFactory) with a
> thread factory that creates virtual threads."*
>
> -
> https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/Executors.html#newVirtualThreadPerTaskExecutor()
>
> And if we look at how `ExecutorService::close()` is specified:
>
> *"Initiates an orderly shutdown in which previously submitted tasks are
> executed, but no new tasks will be accepted. This method waits until all
> tasks have completed execution and the executor has terminated."*
>
> -
> https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/ExecutorService.html#close()
>
> >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.
>
> No, the specification for ExecutorService::close() further states:
>
>
> *"If interrupted while waiting, this method stops all executing tasks as
> if by invoking shutdownNow(). It then continues to wait until all actively
> executing tasks have completed. Tasks that were awaiting execution are not
> executed. The interrupt status will be re-asserted before this method
> returns." *
> Given the above: It looks like you want to use Structured Concurrency
> instead of Executors.newVirtualThreadPerTaskExecutor()
>
> On 2025-12-01 21:16, Stig Rohde Døssing wrote:
>
> 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.
>
> --
> Cheers,
> √
>
>
> Viktor Klang
> Software Architect, Java Platform Group
> Oracle
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20251201/4f81e5b3/attachment.htm>
More information about the loom-dev
mailing list