This content originally appeared on DEV Community and was authored by Manoj Sharma
If you’ve worked with Java ☕️ long enough, you probably remember when thread management felt less like coding and more like careful resource budgeting.
We had to size our thread pools just "right", because
Too small -> and requests would just pile up, and waiting forever.
Too large -> and memory vanished quickly. as each thread reserved a big chunk for itself. Even worse, your CPU would spend all its time just switching between threads instead of doing actually work.
And when I/O came into play — like, blocking on a database call or waiting for a network response — we either had to accept wasted threads or jump into async APIs, callbacks, and CompletableFuture chains.
We all made it work, of course. But it was a constant headache. We spent more time fighting with the concurrency model than we did building the actual feature.
Arrival of Virtual Threads 🚀
Virtual threads, became a stable feature in JDK 21.
They just quietly solved a problem we’d been carrying for years — how to make sequential blocking style code scale without rewriting everything.
Virtual Threads are not new thing anymore; But it’s still worth pausing to think about how they’ve changed the way we approach concurrency.
The goal is simple
Enable scalable concurrency without giving up the familiar, blocking programming model.
And the best part is?
They are still java.lang.Thread instances, but instead of being bound to OS threads, they are managed by the JVM.
How to Create?
Let’s see how virtual threads are created in Java, and how that differs from creating traditional platform threads
Platform Threads
// Platform thread
Thread t = new Thread(() -> {
handleRequest();
});
t.start();
// Executor with fixed pool
ExecutorService pool = Executors.newFixedThreadPool(200);
pool.submit(() -> handleRequest());
pool.shutdown();
Virtual Threads (JDK 21+)
// Single virtual thread
Thread virtualThread = Thread.startVirtualThread(() -> {
handleRequest();
});
// Executor that creates a virtual thread per task
try (ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
virtualThreadExecutor.submit(() -> handleRequest());
}
What’s Really Different Here?
→ Difference in Mechanics: Platform v/s Virtual
A Platform Thread in Java is a one-to-one wrapper around an operating system thread.
Each platform thread corresponds directly to an OS-managed thread, with scheduling, stack allocation, and blocking semantics handled by the operating system.
Because of that one-to-one relationship, creating large numbers of platform threads consumes significant native memory and kernel resources.
When a platform thread performs a blocking I/O call, the underlying OS thread thread sits idle until the operation completes. meaning that system-level thread cannot execute other tasks during that period. That’s why traditional applications rely on "right" fixed-size thread pools — to keep control over how many OS threads exist at once.
A Virtual Thread, changes only the implementation model — not the programming model.
A virtual thread is still an instance of java.lang.Thread, but instead of being tied to an OS thread, it is scheduled by the JVM and runs on top of a small, fixed set of carrier threads (which are regular platform threads).
When a virtual thread performs a blocking operation, the JVM parks that virtual thread and detaches it from its carrier, freeing the carrier to run another virtual thread.
When the blocking operation completes, the virtual thread is resumed — possibly on the same or a different carrier thread.
All of this happens transparently, without changing the semantics of the blocking APIs.
→ Scheduling Responsibility
The key distinction is who manages scheduling and blocking:
- For platform threads, it’s the operating system scheduler.
- For virtual threads, it’s the JVM runtime scheduler, implemented entirely in user space.
Because the JVM controls this scheduling, it can efficiently manage millions of virtual threads, each with a small memory footprint and negligible creation cost compared to platform threads.
This makes virtual threads ideal for I/O-bound, high-concurrency workloads — for example, servers handling thousands of simultaneous connections using blocking code.
→ Important Consideration
It’s really important to note that virtual threads don’t change CPU throughput.
For CPU-bound workloads, where tasks actively use the processor rather than waiting, performance remains limited by available CPU cores.
Virtual threads primarily optimize concurrency and scalability, not computation speed.
We will see this in example...
📈 Performance Comparison: Virtual vs Platform Threads
To get a practical sense of how these differences play out, let’s look at two simple benchmarks - one I/O-bound and one CPU-bound.
I/O-Bound
Testing how both models handle high concurrency with blocking operations.
public class VirtualVsPlatformIoBound {
private static final int IO_TASKS = 10_000;
private static final int IO_SLEEP_MS = 50;
public long measureIO(String label, ExecutorService executor) throws Exception {
long start = System.currentTimeMillis();
for (int i = 0; i < IO_TASKS; i++) {
executor.submit(this::remoteApiCall);
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
long elapsed = System.currentTimeMillis() - start;
System.out.printf("%s finished in %d ms\n", label, elapsed);
return elapsed;
}
private void remoteApiCall() {
try {
Thread.sleep(IO_SLEEP_MS); // Simulated blocking I/O
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// Test
class VirtualVsPlatformIoBoundTest {
@Test
void measureIO() throws Exception {
var ioBound = new VirtualVsPlatformIoBound();
System.out.println("==== I/O-Bound Workload ====");
long ioPlatform = ioBound.measureIO("Platform Threads", Executors.newFixedThreadPool(200));
long ioVirtual = ioBound.measureIO("Virtual Threads", Executors.newVirtualThreadPerTaskExecutor());
double speed = (double) ioPlatform / ioVirtual;
System.out.printf("Speedup(I/O-bound): %.2fx\n", speed);
}
}
I/O-Bound Result
==== I/O-Bound Workload ====
Platform Threads finished in 2689 ms
Virtual Threads finished in 126 ms
Speedup(I/O-bound): 21.34x
CPU-Bound
Testing performance when tasks are purely computational.
public class VirtualVsPlatformCpuBound {
private static final int CPU_TASK_COUNT = 50_000;
public long measureCPU(String label, ExecutorService executor) throws Exception {
long start = System.currentTimeMillis();
for (int i = 0; i < CPU_TASK_COUNT; i++) {
executor.submit(this::cpuIntensiveTask);
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
long elapsed = System.currentTimeMillis() - start;
System.out.printf("%s finished in %d ms\n", label, elapsed);
return elapsed;
}
// Burning CPU
private long cpuIntensiveTask() {
long sum = 0;
for (int i = 0; i < 2_00_000; i++) {
sum += (i * 37L) % 11;
}
return sum;
}
}
// Test
class VirtualVsPlatformCpuBoundTest {
@Test
void measureCPU() throws Exception {
var cpuBound = new VirtualVsPlatformCpuBound();
System.out.println("==== CPU-Bound Workload ====");
long cpuPlatform = cpuBound.measureCPU("Platform Threads", Executors.newFixedThreadPool(200));
long cpuVirtual = cpuBound.measureCPU("Virtual Threads", Executors.newVirtualThreadPerTaskExecutor());
double speed = (double) cpuPlatform / cpuVirtual;
System.out.printf("Speedup(CPU-bound): %.2fx\n", speed);
}
}
CPU-Bound Result
==== CPU-Bound Workload ====
Platform Threads finished in 433 ms
Virtual Threads finished in 454 ms
Speedup(CPU-bound): 0.95x
💡 Yes, Results will vary depending on hardware and blocking time
Observations on Results 🔎
Virtual threads dramatically improve scalability in I/O-bound workloads by releasing carriers during blocking operations, enabling thousands of concurrent tasks with minimal overhead.
For CPU-bound tasks, both perform nearly the same; as I mentioned earlier as well — because computation still competes for physical cores, not thread count.
And Yes, I ran the test multiple times for getting the average/combined result.
🧭 Wrapping Up
Virtual threads don’t replace the fundamentals — they refine them.
In upcoming posts, I’ll dive into Structured Concurrency and other patterns that build on top of this model.
This content originally appeared on DEV Community and was authored by Manoj Sharma
Manoj Sharma | Sciencx (2025-11-09T16:10:24+00:00) Virtual Threads – Comparative Analysis. Retrieved from https://www.scien.cx/2025/11/09/virtual-threads-comparative-analysis/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.