Performance Issues with Virtual Threads + ThreadLocal Caching in Third-Party Libraries (JDK 25)

Jianbin Chen jianbin at apache.org
Sun Jan 25 12:16:35 UTC 2026


Hi Stig,

Thank you very much, and thanks to everyone who participated in this
discussion. I have basically reached the following conclusions:

1. Replace libraries that use ThreadLocal for buffer pools, or refactor
them to object pools wherever possible.
2. If conclusion 1 cannot be completed in the short term, pooling virtual
threads appears to be the only practical option.

I will do my best to implement conclusion 1; until then I will adopt
conclusion 2 as an interim solution.

Thanks again,
Jianbin Chen

Stig Rohde Døssing <stigdoessing at gmail.com> 于2026年1月25日周日 18:58写道:

> Hi Jianbin,
>
> It might be worth considering that depending on unmaintained or poorly
> maintained libraries is a risk for your application (what happens when a
> vulnerability is discovered?), even before virtual threads enter the
> equation.
>
> Creating an object pool should not require any post-Java-8 features, so it
> should be possible to update libraries to be virtual thread friendly
> without dropping compatibility with Java 8. With a bit of abstraction, the
> library could even allow sticking with TLs depending on configuration, see
> for example log4j's Recycler interface
> https://github.com/apache/logging-log4j2/blob/4f474b32751f4ccad67424ca585612584440cd63/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/Recycler.java
>
> However, the question you are asking is essentially "Assuming I can't
> change the libraries, is it okay to pool virtual threads as a workaround?"
>
> The reasoning behind never pooling virtual threads, given in JEP 444, is
> that virtual threads are "cheap and plentiful", allowing for the use of the
> thread-per-task style rather than pooling and reusing threads. With the
> libraries you are using, threads cannot be cheap and plentiful, because the
> TLs make them expensive.
>
> Assuming the libraries can't be changed, you can't use the thread-per-task
> style, so you are forced into pooling. Once you are stuck with pooling, you
> are asking if it's fine for the pool to create virtual threads instead of
> platform threads, because virtual threads are cheaper to create when the
> pool needs to scale.
>
> And it probably is, and there probably aren't better alternatives if we
> stick with your premise that the libraries can't be changed. Pooling
> virtual threads isn't "against the rules", it's just not
> recommended, because ideally you'd change the library rather than hacking
> around the problem by pooling threads.
>
> The drawback to this workaround is that you have to abandon the
> thread-per-task style, which is where a lot of the benefit of virtual
> threads comes from. You end up with something that behaves mostly like a
> normal platform thread pool, except the pool can have more threads than
> usual, and creating those threads is faster than usual. That's good, it's
> just not *as* good as thread-per-task.
>
> As a side note, you might want to consider limiting the pool size, or
> limiting your concurrency in other ways, e.g. via semaphores. Since the TL
> resources are expensive, it doesn't seem like a good idea to have no limit
> on how many threads you can have active at a time.
>
> My 2 cents on your question are that what you are doing is probably fine
> as a short term workaround, but you should really consider putting the
> necessary time into updating those libraries as a longer term solution. You
> will end up with a better result.
>
> Den søn. 25. jan. 2026 kl. 06.57 skrev Jianbin Chen <jianbin at apache.org>:
>
>> Hi Stig,
>>
>> I mostly agree with your view. My emails have been describing a specific
>> scenario: my application runs on JDK 25, but many of the libraries I depend
>> on were developed for JDK 8 or are not very actively maintained. In the
>> short term, pooling virtual threads seems to be the only practical
>> workaround; I don’t see a better alternative right now.
>>
>> One correction I need to make: I did not fix the maximum size of my
>> virtual‑thread pool. That means when the 200 core virtual threads are all
>> in use, the pool’s behavior becomes the same as non‑pooled virtual threads
>> (it will create additional threads). You suggested using platform threads
>> instead, but platform threads have expensive context switching. In my
>> example, if I switch to platform threads then once the 200 core threads are
>> exhausted new platform threads are created, and at the moment those threads
>> are created CPU usage essentially spikes. If you run Java under Kubernetes
>> you’ll be familiar with this: creating new platform threads can instantly
>> consume the cgroup CPU quota, causing the process to be throttled until the
>> next available CPU window. Using a pooled virtual‑thread solution avoids
>> this problem because it does not require creating costly platform threads.
>>
>> Thanks,
>> Jianbin Chen
>>
>> Stig Rohde Døssing <stigdoessing at gmail.com> 于2026年1月25日周日 00:11写道:
>>
>>> Hi Jianbin,
>>>
>>> Sorry to butt in, but I think the question you are asking is a little
>>> odd. You have a library that uses ThreadLocals for reusing expensive
>>> resources (buffers in this case). The way to make such a library work well
>>> with virtual threads is to redesign the library to avoid using TLs in this
>>> manner. For example, you could make the library keep a pool of these
>>> resources for reuse in a non-TL structure, like concurrent
>>> maps/lists/queues.
>>>
>>> But once you set the limitation that the library can't be adjusted, you
>>> are forced into awkward workarounds. This is because the main advantage of
>>> virtual threads is to allow you to write code in a thread-per-task style,
>>> but the presence of these TLs makes threads precious resources that must be
>>> reused across tasks, which loses you the ability to use virtual threads in
>>> this way.
>>>
>>> If you are unable to adjust the library and really want to use virtual
>>> threads for part of your code, an option is to isolate the TL-using code so
>>> it runs on a platform thread pool. You would then write most of your code
>>> in thread-per-task style with virtual threads, but make the virtual threads
>>> hand off work that needs the TLs to the thread pool, blocking the virtual
>>> thread until that work completes.
>>>
>>> If that is not an option, and you don't want that kind of handoff, you
>>> are forced to create a pool of threads, as you found. But at that point, I
>>> don't really understand why you want to use virtual threads at all. Once
>>> you are making a pool of 200 threads you reuse, it doesn't really matter if
>>> those threads are virtual or platform threads. You are forced to abandon
>>> the thread-per-task style either way.
>>>
>>> I don't think there is a great solution that will let you use
>>> thread-per-task style virtual threads with a library that uses TLs for
>>> resource reuse. The best you are likely to be able to do is various
>>> workarounds, with various drawbacks. It might be better to aim for
>>> reworking the library, and sticking with platform threads until you can do
>>> that?
>>>
>>> Den lør. 24. jan. 2026 kl. 14.14 skrev Jianbin Chen <jianbin at apache.org
>>> >:
>>>
>>>> Hi Alan,
>>>>
>>>> I ran my example on JDK 21 because it uses Thread.sleep. In an earlier
>>>> message on the mailing list I learned that virtual‑thread performance on
>>>> JDK 25 was worse for this kind of scenario compared with JDK 21, and that
>>>> the issue is supposed to be fixed in JDK 25.0.3 — which has not been
>>>> released yet.
>>>>
>>>> That said, this does not affect the main point of my message: I’m
>>>> asking for advice about using pooled virtual threads to work around
>>>> third‑party libraries that implement buffer pools via ThreadLocal.
>>>>
>>>> Thank you,
>>>> Jianbin Chen
>>>>
>>>> Alan Bateman <alan.bateman at oracle.com> 于2026年1月24日周六 16:34写道:
>>>>
>>>>>
>>>>>
>>>>> On 24/01/2026 05:55, Jianbin Chen wrote:
>>>>> > :
>>>>> >
>>>>> > I constructed the Executor directly with
>>>>> > Executors.newVirtualThreadPerTaskExecutor();
>>>>> > however, the run results still show that the pooled virtual‑thread
>>>>> > behavior outperforms the non‑pooled virtual threads.
>>>>>
>>>>> This looks like it is benchmarking Thread.sleep so a different topic
>>>>> to
>>>>> that of libraries that are caching objects in thread locals.
>>>>>
>>>>> For the Thread.sleep test then it would easier to discuss if converted
>>>>> to a JMH benchmark as there are warmup issues in the test you
>>>>> included.
>>>>> Also just to note that the Thread.sleep implementation has changed
>>>>> significantly changed since JDK 21 so you will see very different
>>>>> results with JDK 25 runs (some of the messages in the discussion speak
>>>>> of JDK 21, the subject line in the mails say "JDK 25", so I'm guessing
>>>>> you might be testing both).
>>>>>
>>>>> -Alan
>>>>>
>>>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20260125/d2955474/attachment.htm>


More information about the loom-dev mailing list