Performance Issues with Virtual Threads + ThreadLocal Caching in Third-Party Libraries (JDK 25)
Jianbin Chen
jianbin at apache.org
Sun Jan 25 08:50:55 UTC 2026
Hi Robert,
I don’t understand why you think I’m trolling. Also, you haven’t offered a
concrete solution to the real problem I’m facing. I provided many examples
— were you looking at them? Do I need to be trolling about this issue? My
example is a real‑world case; do you expect me, by myself, to update all
the third‑party libraries I use so they adopt newer JDK features and
guarantee they no longer use ThreadLocal? I only want to discuss whether
pooling virtual threads is a best practice when you depend on libraries
that use ThreadLocal for buffer pooling.
Regarding your concern that my example would allocate 16GB at once: I set
both Xms and Xmx to 2880m. The code is as follows:
```java
public static void main(String[] args) throws InterruptedException {
int tasks = 2000000;
// Create an executor that spawns a new virtual thread for each task
Executor executor = Executors.newThreadPerTaskExecutor(
Thread.ofVirtual().name("test-", 1).factory());
// Create a virtual-thread-based thread pool with large max size and
SynchronousQueue
// This allows unbounded growth when needed, but reuses idle threads up
to keepAliveTime
Executor executor2 = new ThreadPoolExecutor(
200, // core pool size
Integer.MAX_VALUE, // maximum pool size (effectively
unbounded)
0, // keep-alive time
java.util.concurrent.TimeUnit.SECONDS,
new SynchronousQueue<>(), // handoff queue — forces new thread
creation when pool is busy
Thread.ofVirtual().name("test-threadpool-", 1).factory());
// Warm-up / pre-heat phase to allow JIT compilation and thread creation
for (int i = 0; i < 10100; i++) {
executor.execute(() -> {
// Simulate I/O wait
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
executor2.execute(() -> {
// Simulate I/O wait
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
// Wait long enough to ensure JIT compilation and warm-up are complete
Thread.sleep(5000);
long start = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(tasks);
for (int i = 0; i < tasks; i++) {
executor.execute(() -> {
// Simulate I/O wait
try {
Thread.sleep(100);
countDownLatch.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
countDownLatch.await();
System.out.println("thread time: " + (System.currentTimeMillis() -
start) + " ms tasks: " + tasks);
start = System.currentTimeMillis();
CountDownLatch countDownLatch2 = new CountDownLatch(tasks);
for (int i = 0; i < tasks; i++) {
executor2.execute(() -> {
// Simulate I/O wait
try {
Thread.sleep(100);
countDownLatch2.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
countDownLatch2.await(); // Fixed: should await countDownLatch2 (not
the first one)
System.out.println("thread pool time: " + (System.currentTimeMillis() -
start) + " ms tasks: " + tasks);
}
```
When the number of concurrent tasks is between 100,000 and 2,000,000,
non‑pooled virtual threads perform better; when it is greater than two
million or less than 100,000, the pooled approach performs better. However,
I don’t want to continue discussing this particular example.
Regards,
Jianbin Chen
robert engels <robaho at me.com> 于2026年1月25日周日 14:59写道:
> No. You aren’t listening. Your test case is broken. It will allocate 16gb
> of heap almost immediately. I’ve tried to explain this to you multiple
> ways. I think you are trolling.
>
> On Jan 25, 2026, at 12:12 AM, Jianbin Chen <jianbin at apache.org> wrote:
>
>
> Hi Alan,
>
> Thanks for the heads up — I’ll try running the performance tests again on
> the JDK 26 EA build.
>
> That said, let’s continue the discussion around the main topic of this
> thread. I understand and largely agree with most people’s recommendations
> and the reasons why virtual threads shouldn’t be pooled. However, those
> recommendations generally assume that both our application and the
> third‑party libraries it depends on have been adapted to newer JDK features
> — for example, replacing ThreadLocal‑based buffer pools with proper object
> pools — and I fully appreciate the benefits of making those changes.
>
> In reality it’s often much harder: many third‑party libraries must remain
> compatible with older JDK 8 users, so they are not eager to adopt newer
> platform features. That leaves users like us in a difficult position. For
> example, when JDK 21 shipped I eagerly adopted virtual threads, but then
> suffered from issues caused by third‑party libraries using synchronized,
> Object.wait, etc. I traced the pinned‑thread causes to such libraries —
> take apache commons‑pool as an example: you can see PRs addressing
> synchronized/Object.wait refactors here:
> https://github.com/apache/commons-pool/pulls — some of those PRs were
> opened two or three years ago and are still unapproved. Aren’t these kinds
> of problems part of the motivation behind JEP 491?
>
> Given this reality, the only practical short‑term approach available to me
> appears to be a compromise: pool virtual threads but do not set a fixed
> maximum thread count. That way I can both limit buffer waste (which hurts
> performance) and still gain the throughput benefits of virtual threads.
>
> Best regards,
> Jianbin Chen
>
> Alan Bateman <alan.bateman at oracle.com> 于2026年1月25日周日 00:42写道:
>
>>
>>
>> On 24/01/2026 13:07, Jianbin Chen wrote:
>> > 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.
>>
>> I assume this is about JDK-8370887 [1], which may be an issue in some
>> usages but I don't think has come up in this thread. For your
>> Thread.sleep benchmark then maybe you can try it with the JDK 26 EA
>> builds [2] where you know that issue has been fixed. As I said, I think
>> that benchmark will need a bit of work (esp. on warmup) to get useful
>> data.
>>
>>
>> >
>> > 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
>> The advise is to not pool virtual threads. If a library is performing
>> poorly because it assumes execution on a pooled thread then all we can
>> suggest is to work with the maintainer of that library on the issue.
>> Note that the JDK removed several usages of thread locals that were
>> caching byte[] and other objects. That caching was beneficial a long
>> time ago but not in recent recent/releases with significantly improved
>> memory management and GC.
>>
>> -Alan
>>
>> [1] https://bugs.openjdk.org/browse/JDK-8370887
>> [2] https://jdk.java.net/26/
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20260125/0af24e45/attachment.htm>
More information about the loom-dev
mailing list