How did Java evolve from:
“1 OS thread = 1 Java thread”
to VIRTUAL THREADS handling millions of requests efficiently… 🧵
Initially Java used PLATFORM THREAD, which meant 1 Java Thread is mapped to one OS Thread.
Simple model.
Request comes → create thread → do work → send response.
Worked fine initially.
Until traffic exploded.
10 users.
100 users.
10k users.
Now JVM creates thousands of OS threads. Problem?
OS threads are EXPENSIVE.
Each thread needs:
- stack memory
- kernel scheduling
- CPU coordination
- context switching
- thread metadata
And default stack space for a thread alone can be ~1MB.
So, 100k threads = 100GB of stack space 💀
But memory wasn’t even the biggest problem. Most backend threads were not computing. They were WAITING.
Waiting for:
- DB
- APIs
- network
- disk
That means, thousands of expensive OS threads just sleeping idle.
Even worse, CPU starts spending huge time in CONTEXT SWITCHING.
Saving thread state.
Loading another.
Flushing caches.
Server starts burning CPU doing “thread management” instead of actual work.
So industry moved towards:
- async programming
- callbacks
- event loops
- reactive systems
Because thread-per-request stopped scaling. But async code became nightmare fuel.
Then Java introduced:
VIRTUAL THREADS.
And the idea was insane:
“What if threads were cheap?”
Now, many Java virtual threads run on few real OS threads.
Instead of:
1 Java thread = 1 OS thread
we can now get:
Millions of virtual threads mapped onto maybe 8 carrier threads.
Carrier thread = actual OS thread underneath.
You as a passenger are virtual thread riding that Uber ride which is carrier thread. Once you reach your stop(done with the task) or do wait for your another friend (blocking task) , it will save your context of that state as a chunk and store it in heap (which we normally used to do on stack) and will drop u off instantly.
Now that Uber driver will instantly pick up another passenger(virtual thread), you'll have to wait until your friend comes(blocking task completes) before jumping into another ride.
In technical terms,
Virtual thread uses carrier thread while actively running.
But when it hits blocking call:
db.fetch()
socket.read()
JVM does something magical.
Instead of blocking OS thread,
it PAUSES virtual thread,
saves its execution state,
and immediately frees carrier thread.
Carrier thread now starts executing another virtual thread.
This is the breakthrough.
Earlier:
blocking call blocked whole OS thread.
Now:
only virtual thread pauses.
Internally JVM uses:
- continuations
- heap stored stacks
- async IO underneath
- scheduler managing remounting
So virtual threads basically make:
ASYNC EXECUTION
look like
NORMAL BLOCKING CODE.
You write simple synchronous code.
JVM secretly handles scalability magic underneath.
But there’s one catch:
PINNING.
Some operations cannot release carrier thread safely.
Example:
- synchronized blocks
- JNI/native calls
Then carrier thread also gets stuck which can cause issues with scalability...
Still…
Virtual threads are one of the biggest backend engineering upgrades in years because they combine:
simple code massive concurrency
without callback hell.
Stack is basically OS-thread associated memory while heap is JVM managed that's what gave JVM full flexibility over concurrency management.
Backend evolution basically became:
1 thread per request
→ thread explosion
→ async/event loops
→ callback hell
→ reactive programming
→ virtual threads making blocking code scalable again.
Do share and repost if you like this way of learning concepts 🫶
#java #virtualthreads #concurrency #tech #engineering #techblogs