Java’s New Superpower: Virtual Threads vs. Old OS Threads
Understanding the big difference with easy examples, code, and real numbers. No expert knowledge needed!
Java’s New Superpower: Virtual Threads vs. Old OS Threads
Understanding the big difference with easy examples, code, and real numbers. No expert knowledge needed!

Hello everyone!
If you use Java, you probably heard about “threads”. Threads help our applications do many things at the same time. For a long time, we only had one type of thread: OS Threads. But now, Java has a new, powerful type: Virtual Threads.
What is the difference? Why is everyone excited? Let’s understand it with a simple story.
To understand the difference, let’s imagine two cafes. Both want to serve many customers.
Cafe 1: The OS Thread Cafe This cafe has a few service counters. Behind each counter is one worker. Let’s say there are 10 workers (OS Threads).
- A customer (a task) comes to a counter. The worker takes the order (e.g., “one cappuccino”).
- The worker then starts making the coffee. The coffee machine is slow and takes one minute. This “slow coffee machine” represents waiting for something external and slow, like a network request to another server or fetching data from a database.
- During this whole minute, the worker just stands and waits for the machine. He cannot take any other orders. He is completely blocked.
If 10 customers arrive, all 10 workers are busy waiting for their coffee machines. When the 11th customer arrives, they must wait in a long line until one of the workers becomes free. This is not efficient. The cafe can only serve a few customers at a time.

Cafe 2: The Virtual Thread Bistro This bistro is smarter. It has the same small kitchen crew (let’s say 4 OS Threads). But it also has hundreds of very cheap and fast waiters (Virtual Threads).
Here is how it works:
- A customer (a task) arrives and a waiter (a Virtual Thread) immediately takes their order.
- The waiter runs to the kitchen, gives the order slip to the kitchen crew, and does not wait!
- The waiter immediately goes back to the entrance to serve a new customer.
- The small kitchen crew (the OS Threads) handles the actual, slow operations. This “slow operation” is again, waiting for the coffee machine or database. The kitchen crew members are the only ones who actually “block” themselves on these slow tasks.
- When the food is ready, the kitchen calls out. The original waiter hears this, runs to get the food, and serves it to the customer.
In this bistro, the waiters are never blocked. While the kitchen is busy, the waiters are busy taking more orders. A very small kitchen crew can handle hundreds of customer orders at the same time, thanks to the large number of efficient waiters.

This story perfectly explains the concept:
- Virtual Threads (the waiters) are lightweight. You can have thousands. They start a task and hand it off when there is a wait.
- OS Threads (the kitchen crew) are the real workers. They are heavy and limited. Virtual threads allow them to be busy with real work, not just waiting.
Now, let’s turn our cafe story into real Java code. We will see exactly how the “blocked worker” and the “efficient waiter” concepts work.
The Old Way: The OS Thread Cafe
First, let’s build our OSThreadCafe. We will have 200 workers (OS Threads) and 10,000 customers (tasks). Each customer’s order takes 1 second to prepare (Thread.sleep).
The code is simple. We use a FixedThreadPool, which means we have a fixed number of workers.
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class OSThreadCafe {
public static void main(String[] args) throws InterruptedException {
// Our cafe has 200 workers (OS Threads)
int numberOfWorkers = 200;
int numberOfCustomers = 10_000;
System.out.println("--- OS Thread Cafe Simulation ---");
System.out.printf("Starting %d tasks with %d workers (OS Threads)...\n", numberOfCustomers, numberOfWorkers);
long startTime = System.currentTimeMillis();
// Create a pool of 200 OS threads (our workers)
try (var executor = Executors.newFixedThreadPool(numberOfWorkers)) {
// Give 10,000 tasks (customers) to our workers
IntStream.range(0, numberOfCustomers).forEach(i -> {
executor.submit(() -> {
// The worker "makes the coffee" and waits 1 second.
// This Thread.sleep() simulates waiting for a slow external resource (e.g., database query, network call).
// During this time, the OS Thread worker is blocked and cannot do anything else.
try {
Thread.sleep(Duration.ofSeconds(1));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
});
// We must shutdown the executor and wait for all tasks to finish
// This is like waiting for the cafe to close after serving everyone.
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
System.out.println("All tasks finished.");
System.out.printf("Total time taken: %d ms (approx. %d seconds)\n", duration, duration / 1000);
}
}
The New Way: The Virtual Thread Bistro
Now, let’s visit the VirtualThreadBistro. Here, we create a new, super-lightweight “waiter” (Virtual Thread) for every single customer.
The code is almost the same! The only important change is how we create the ExecutorService.
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class VirtualThreadBistro {
public static void main(String[] args) throws InterruptedException {
int numberOfCustomers = 10_000;
System.out.println("--- Virtual Thread Bistro Simulation ---");
System.out.printf("Starting %d tasks with virtual waiters...\n", numberOfCustomers);
long startTime = System.currentTimeMillis();
// The magic is here! A new virtual thread for every task.
// This is our army of efficient waiters.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Give 10,000 tasks (customers) to our waiters
IntStream.range(0, numberOfCustomers).forEach(i -> {
executor.submit(() -> {
// The waiter gives the order and "waits" 1 second.
// Like before, Thread.sleep() simulates waiting for a slow external resource.
// BUT, the virtual thread itself is NOT blocked. It "unmounts" from its underlying OS thread,
// allowing that OS thread to be used by another virtual thread.
try {
Thread.sleep(Duration.ofSeconds(1));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
});
// Wait for the bistro to serve all customers.
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
System.out.println("All tasks finished.");
System.out.printf("Total time taken: %d ms (approx. %.1f seconds)\n", duration, duration / 1000.0);
}
}
The Benchmark:
I ran both programs on my computer. Here is what the output looks like.
Running the OS Thread Cafe:
--- OS Thread Cafe Simulation ---
Starting 10000 tasks with 200 workers (OS Threads)...
All tasks finished.
Total time taken: 50215 ms (approx. 50 seconds)
Running the Virtual Thread Bistro:
--- Virtual Thread Bistro Simulation ---
Starting 10000 tasks with virtual waiters...
All tasks finished.
Total time taken: 1188 ms (approx. 1.2 seconds)
Why the Big Difference?
The results are incredible! But why?
- OS Thread Cafe: We had only 200 workers for 10,000 customers. A worker was busy for 1 second with each customer and could not do anything else. So, the system worked in batches.
- Calculation: 10,000 customers / 200 workers = 50 batches.
- Each batch took 1 second. So, the total time is 50 batches * 1 second = 50 seconds. The result is exactly what we expected!
- Virtual Thread Bistro: We created 10,000 waiters (virtual threads) almost instantly. When a waiter had to “wait” for the 1-second coffee order, they didn’t actually block a real system worker. They just told the kitchen (the JVM), “Wake me up in one second,” and stepped aside. This allowed all 10,000 orders to be “in progress” at the same time.
- The total time was just a little more than 1 second, which is the time for the longest task.
This shows the power of not blocking. Virtual threads allow our applications to handle thousands of waiting tasks with very few real OS threads, just like our smart bistro.
When Do Virtual Threads Shine? (I/O-Bound vs. CPU-Bound)
It’s crucial to understand when virtual threads are most power+ful. Our example uses Thread.sleep(), which is a perfect stand-in for tasks that spend most of their time waiting for something external, not actively doing computations. This is known as I/O-bound work.
I/O-Bound Tasks (Where Virtual Threads Excel!): These are tasks that involve a lot of waiting for external systems. Examples include:
- Making requests to a database (waiting for the query result).
- Calling an external API (waiting for the response from another server).
- Reading from or writing to a file (waiting for the disk).
- Waiting for user input.
- Think of the waiters in our bistro: They are perfect for taking orders and waiting for the kitchen to finish slow external processes.
CPU-Bound Tasks (Where Virtual Threads Offer No Magic): These are tasks that spend most of their time actively using the CPU for computations. Examples include:
- Heavy mathematical calculations.
- Image processing or video encoding.
- Complex data analysis.
- Think of the kitchen crew just trying to solve a very difficult puzzle: Having more waiters doesn’t make them solve the puzzle faster. If your task is purely about raw computational power, you still need more actual CPU cores or more traditional OS threads to utilize those cores. Virtual threads won’t magically speed up a single, long-running calculation. In fact, creating too many virtual threads for CPU-bound tasks can even introduce overhead and slow things down.
Our example with Thread.sleep() perfectly demonstrates the I/O-bound scenario where virtual threads provide massive benefits by efficiently handling thousands of concurrent waiting tasks.
Important Considerations: Are There Any Downsides?
While virtual threads are an incredible leap forward, it’s fair to mention a couple of things the community is discussing:
- Debugging Challenges: Imagine having 10,000 waiters (virtual threads) running around. If one of them makes a mistake, trying to track down exactly which waiter it was and what they were doing can be a bit more complex than with only 200 workers. Debugging tools (like those in IDEs such as Eclipse) are still evolving to better handle the sheer number of virtual threads, and you might experience performance slowdowns when trying to inspect all of them in a debugger. The good news is that tool developers are actively working on improving this.
- Tooling & Observability: Similarly, monitoring tools that show you the state of your application might need updates to properly visualize and manage so many concurrent virtual threads without getting overwhelmed.
These are not reasons to avoid virtual threads, but rather points to be aware of as the ecosystem continues to mature around this powerful new feature.
Summary: Virtual threads are a game-changer for modern Java applications, especially those that frequently interact with slow external systems (I/O-bound tasks). They allow you to write simple, synchronous-looking code that can handle massive concurrency, making your applications more responsive and scalable than ever before. Just remember their sweet spot and be mindful of the evolving tooling!
메타데이터
- post_id
- 57dfd624d823
- slug
- javas-new-superpower-virtual-threads-vs-old-os-threads-57dfd624d823
- url
- https://medium.com/@bulutruzgaremir/javas-new-superpower-virtual-threads-vs-old-os-threads-57dfd624d823
- canonical_url
- https://medium.com/@bulutruzgaremir/javas-new-superpower-virtual-threads-vs-old-os-threads-57dfd624d823
- author_url
- https://medium.com/@bulutruzgaremir
- status
- ok
- fetched_at
- 2026-08-24 04:50:48