Java 21 introduced Virtual Threads, one of the most impactful JVM features in recent years. Virtual threads fundamentally change how we think about concurrency and scalability in Java.
Before diving into code, let’s first understand why virtual threads exist.
Platform Threads vs Virtual Threads
Platform Threads
Platform threads are what Java has traditionally used.
- A platform thread maps 1:1 to an OS thread
- When a platform thread is created, an OS thread is also created
- That OS thread is occupied for the entire lifetime of the Java thread
- Number of platform threads = number of OS threads
Because OS threads are expensive, we must carefully manage them using thread pools.
Virtual Threads
Virtual threads work very differently.
- A virtual thread is not tied to a single OS thread
- When a virtual thread performs a blocking I/O operation, the JVM:
- Suspends the virtual thread
- Reuses the OS thread to run other virtual threads
- Once the blocking operation completes, the virtual thread is resumed
This behavior is fully managed by the JVM, not the operating system.
👉 A good analogy:
Virtual threads are to threads what virtual memory is to RAM.

Lightweight by Design
Virtual threads are intentionally designed to be:
- Lightweight
- Cheap to create
- Optimized for I/O-heavy workloads
They are not meant to run long, CPU-intensive computations.
Virtual Threads Are About Scale, Not Speed
A very important point to understand:
Virtual threads are not faster threads.
They do not improve raw performance.
Instead, they provide:
- Massive scalability
- High throughput
- Simpler concurrency models (no callback hell, no reactive complexity)
Creating a Virtual Thread
Creating a virtual thread is straightforward.
Thread.Builder builder = Thread.ofVirtual().name("Virtual Thread");
Runnable runnable = () -> {
System.out.println("Running runnable thread");
};
Thread t = builder.start(runnable);
System.out.println("Thread name: " + t.getName());
t.join();
No special configuration. No custom schedulers. The JVM handles everything.
Virtual Threads with ExecutorService
Java 21 introduced a new executor factory method:
Executors.newVirtualThreadPerTaskExecutor()
Example:
try (ExecutorService myExecutor =
Executors.newVirtualThreadPerTaskExecutor()) {
Future<?> future = myExecutor.submit(() ->
System.out.println("Executor Service thread running")
);
future.get();
System.out.println("Main thread running");
}
Key idea:
- Each submitted task gets its own virtual thread
- No pooling required
Why Virtual Threads Are Powerful
- Virtual threads are implemented by the JVM, not the OS
- We can create millions of virtual threads
- Thread creation is no longer a scalability bottleneck
This is the real power of virtual threads.
Should We Create Virtual Thread Pools?
Short answer: No.
Why thread pools exist:
- Platform threads are resource-intensive
- OS threads must be reused carefully
Virtual threads change this equation.
- Virtual threads are plentiful
- Creating a pool for them defeats their purpose
That’s why:
Executors.newVirtualThreadPerTaskExecutor()
creates a new virtual thread for every task.
When Do Virtual Threads Actually Help?
According to Oracle’s documentation:
If your application is not creating at least thousands (10,000+) virtual threads,
you are probably not benefiting from virtual threads.
They shine in:
- High-concurrency
- I/O-bound systems (APIs, DB calls, messaging)
Limiting Concurrency with Virtual Threads
Even though virtual threads are cheap, external systems are not.
To limit concurrent access (DB, APIs, downstream services), use a Semaphore.
Semaphore semaphore = new Semaphore(10);
semaphore.acquire();
try {
// call virtual-thread-based service
} finally {
semaphore.release();
}
This gives you:
- Unlimited virtual threads
- Controlled concurrency
Best of both worlds.
ThreadLocals and Virtual Threads
Yes, virtual threads support ThreadLocal variables.
If you are already storing request context or metadata in ThreadLocal, it will continue to work.
However, for safer and more efficient context propagation, consider using:
- Scoped Values (introduced in newer Java versions)
They integrate much better with modern concurrency models.
Final Thoughts
Virtual threads simplify concurrency in Java dramatically:
- Write blocking code
- Scale like non-blocking systems
- No reactive complexity
- No thread pool tuning nightmares
In many ways, virtual threads bring Java back to what it does best:
simple, readable, and scalable code.
Leave a comment