Non-obvious behavior in newVirtualThreadPerTaskExecutor

Viktor Klang viktor.klang at oracle.com
Mon Dec 1 21:19:26 UTC 2025


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/e6abbacc/attachment-0001.htm>


More information about the loom-dev mailing list