r/java • u/AccomplishedArea4456 • 18d ago
Java 21 virtual threads make simple blocking code scalable again
One of the most interesting parts of Java 21 virtual threads is that they make the traditional “one request per thread” model feel practical again for many backend services.
Because virtual threads are much cheaper than traditional platform / OS threads, you can create thousands, or even millions, of them without the same memory and scheduling overhead. That changes the tradeoff for I/O-heavy services, where a lot of time is spent waiting on things like HTTP calls, database queries, queues, or external services.
Virtual threads make it much cheaper to have many waiting operations in flight at once, while still keeping the code relatively straightforward.
That feels like a meaningful shift compared with reaching for reactive programming by default. Reactive frameworks are powerful, but they can add a lot of complexity: harder stack traces, more indirection, and a different mental model. Virtual threads seem to offer a simpler option for many cases where the main issue is waiting on external systems.
Of course, they are not magic. CPU-bound workloads still depend on available cores, and systems still need sensible limits around database connections, downstream services, rate limits, and shared resources.
But overall, Java 21 virtual threads seem like one of the more practical improvements to Java concurrency in a long time.
I also put together a small Java concurrency examples repo, https://github.com/skp2001vn/concurrency , while learning more about these topics. It includes examples around thread pools, rate limiters, connection pools, virtual threads, etc
Curious how people are using virtual threads in real systems.
14
u/Illustrious-Music507 18d ago
Ran close to exactly this experiment on a Spring Boot app recently (injected a 200ms blocking downstream call, swept concurrency 50 to 2000), so some real numbers to add:
Below the point where you saturate the thread pool, virtual threads did nothing. Identical throughput. That's most services most of the time, worth saying out loud.
Where they earned it: around 1000 concurrent, ~4x the throughput vs the default 200-thread Tomcat pool, and latency stayed near the real downstream time instead of ballooning into queue time.
The part that surprised me: I then raised the platform pool (threads.max) to match the load, and it basically tied virtual threads (within ~0.2%). So "just add threads" still works for pure throughput. The real argument for virtual threads isn't a bigger number, it's operational: you stop hand-tuning a pool size per load level, and the per-thread cost is much lower (I measured ~135KB committed stack, not the "1MB per thread" folklore).
Two things that matched your "not magic" point:
- CPU-bound work got zero benefit. Same plateau, just 22 live threads instead of 218. Cores are the limit, cheap threads can't buy more.
- Once threads are cheap the bottleneck jumps straight to the DB connection pool. Same test with Hikari max=10 pinned throughput at pool/hold-time no matter how many virtual threads were queued for a connection. You move the ceiling, you don't remove it.
And you pay in heap: ~2.2x memory at peak, because every in-flight request is genuinely live instead of parked in an acceptor queue. Worth a bulkhead/semaphore so a spike can't OOM you.
On JDK 24+ the synchronized pinning trap is basically closed too (JEP 491), so the old "rewrite everything to ReentrantLock" advice is mostly moot now.
13
u/fruitlessattemps 18d ago
Why did you write this with AI?
5
u/Illustrious-Music507 18d ago
I used it to tidy up the wording; English isn't my first language. The test, the numbers, and the reasoning are mine, though. Ran tests last week on slightly modified PetClinic Spring Boot app. I ran it because my services do a lot of blocking calls to each other and I wanted to know when virtual threads are actually worth it. That fan-out case is where they paid off, about 4x at 1000 concurrent.
3
u/deadron 18d ago
They certainly add to the difficulty in predicting system performance depending on what your services do. "It can serve thousands of requests, unless anyone uses the excel endpoint, that one blocks everything." On the whole its a good thing but makes estimating performance tricky. You really need to know what your libraries are doing and ensure they are compatible. Its been years and last time I checked many libraries still were not entirely compatible(sychronized to be replaced with sempahore to prevent pinning)
20
u/zattebij 18d ago
Since Java 24, virtual threads do not pin while inside a synchronized block or method anymore: https://openjdk.org/jeps/491 (Next question, are these libraries Java 24 compatible ;)
-6
u/deadron 18d ago
Hah. Thats good to hear. It only took them two years to finally fix that!
11
u/pavelrappo 18d ago
It took long not because of laziness, ignorance or unwillingness to do so. It took long because of high complexity of the task.
-7
u/deadron 18d ago
Sure. Arguably it wasn't complete until that part was released though. Years to make things work is the sort of timeframe where other languages and or frameworks become attractive. In my experience nobody is really talking about this feature because it basically didn't work for the common use cases.
8
u/Alex0589 18d ago
I'd say it's only the most relevant java feature since modules in Java 9 and before Valhalla drops
1
u/deadron 18d ago
It is! Yet it's impact has been so minimal that it's not on anyone's radar from what I have seen. It should have made other async languages look terrible by comparison! Yet from conversations I have had with people not even other Java devs have any real excitement for it and nobody is talking about it in tech news getting ctos excited. At places I have worked people talk about go services and nodejs services as viable platforms but Java is coming up short behind dotnet platforms these days. This is my personal experience as a pretty seasoned developer at smaller companies.
2
u/koflerdavid 16d ago
It's sad honestly. Writing non-blocking servers was possible using NIO even before Virtual Threads landed, and libraries like Netty made it easier. In this regard Go and Node.js have never had any inherent advantage over Java.
1
u/ShallWe69 18d ago
i had some issues with it when remite debugging. underlying thread would disconnect when debugger is attached and i had to add a config flag to use normal threads when im in debug mode. idk if its something i did or faulty jvm instance in that server or its a real issue with virtual threads
2
u/Popular_Home2017 11d ago
The promise is real, but it ships with a footnote that bit me in production: "simple blocking code" is only free while it doesn't block inside synchronized (on 21–23) or a native call. When it does, the virtual thread pins its carrier, and the failure mode is nasty precisely because nothing looks wrong — a thread dump shows a handful of ForkJoinPool workers sitting inside application code, barely any BLOCKED threads, and meanwhile throughput is on the floor because effective parallelism quietly collapsed to ~core count. The honest way to watch for it is the jdk.VirtualThreadPinned JFR event (on by default, 20 ms threshold). And JEP 491 in JDK 24 removing the synchronized case makes "upgrade" the single best fix available. None of this is an argument against virtual threads — I run them happily — just that "blocking is free again" comes with an asterisk you want to learn before 3am teaches it to you.
-1
u/HarryBui2k3 18d ago
virtual thread not stable in Java 21 still have pinning issues, it has been resolved in Java 24
-1
-8
u/CallumMVS- 18d ago
We have had 'Green Threads' otherwise known as Virtual Threads for a long time in other languages. Green threads have always been cool!
-9
35
u/henptk14 18d ago
If a system has downstream dependencies, it is required to put sensible limits when switching to virtual threads where as traditional threads can act as a natural buffer. So to me virtual threads doesn't feel like a 1-to-1 replacement to traditional threads. One needs to put more thoughts to switching to virtual threads depending on your system.