In Java, threads are the foundation of concurrency. They allow multiple tasks to run concurrently, making applications more responsive. making applications more responsive. For year, Java have relied on platform thread, where every Java thread be mapped to an operating system thread. nonetheless, this work okay for CPU-intensive workload, but it become a trouble when an application computer program needs to handle tens or hundreds of thousands of simultaneous I/O mathematical operation — for example, World Wide Web request, database calls, or message processing.
To avoid creating thousands of OS threads (which are expensive in terms of memory and context-switching), developers have been forced to use thread pools, asynchronous code, reactive frameworks, and complicated callback-based designs. These solutions work, but they make the code harder to read, debug, and maintain.
To solve this, Java introduced virtual threads (Project Loom) — a lightweight, user-mode implementation of threads that behave like normal java.lang. Hence, Thread objects, but are not tied 1:1 to OS threads. Virtual thread is cheap to make, schedule, and block. When a virtual thread performs a blocking operation, the JVM simply parks it and frees the underlying carrier thread, which can immediately run another virtual thread. nonetheless, The final result: millions of concurrent tasks without the complexity of async code or reactive frameworks.
Creating a virtual thread looks almost the same as creating a regular thread — the difference is how it’s started:
Thread vt = Thread.startVirtualThread(() -> {
System.out.println("Running inside a virtual thread: " + Thread.currentThread());
});
So how do virtual threads really compare to platform threads? Let’s put them side by side.
At first glance, a virtual thread looks just like any other Java thread — you can create it, start it, join it, and use the same APIs (Thread, Executor Service, CompletableFuture, etc.). Furthermore, the real difference lies under the hood — in how the JVM schedules and manages them.
There are a couple of ways to create virtual threads in Java. Both are simple, but they serve slightly different use cases — direct thread creation and task execution through an executor.
The Thread.ofVirtual() factory provides a fluent API to create and start a virtual thread manually.
This is ideal when you just want to spin up one or a few lightweight threads for demonstration or fine-grained control.
Thread vThread = Thread.ofVirtual(). start (() -> {
System.out.println("Running in: " + Thread.currentThread());}
);
// Wait for it to finishvThread.join();
Thread.ofVirtual() returns a Thread.Builder, which you can utilize to configure and start a virtual thread.
When start () is called, the JVM schedules the virtual thread on a carrier thread from its internal pool.
join () works exactly like it does with a normal thread — same API, no learning curve.
For most real applications, you’ll want to manage tasks instead of threads directly.
This is where the virtual thread executor shines — it creates a new virtual thread for each submitted task and automatically cleans it up when the task completes.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Submit multiple tasks
for (int i = 0; i < 5; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " running in: " + Thread.currentThread());
Thread.sleep(1000); // Simulate work
return taskId;
});
} // Executor closes automatically at the end of the try-with-resources block}
Each task runs in its own virtual thread, so you don’t need to worry about thread pools, tuning, or queue management.
Since virtual threads are cheap, this model scales effortlessly — millions of short-lived tasks are feasible.
You can still use all standard concurrency utilities (Future, CompletableFuture, etc.) on top of it.
Conceptually, virtual threads sound outstanding — but allow’ s see what that means in genuine number.
A classic pain point with platform threads is their high memory and scheduling overhead. Even a few one thousand of them can force a system of rules to its limits. moreover, Consequently, Virtual thread, on the other, be designed to handle millions of concurrent tasks with ease.
Furthermore, Let’s test that difference with a simple benchmark.
Create and run 1 million lightweight tasks, each doing a trivial job (e.g., sleeping for a short time).
We’ll measure:
How long it takes to start and complete all tasks.
Whether the system stays responsive or starts choking.
public class PlatformThreadBenchmark {
public static void main(String[] args) throws InterruptedException {
int taskCount = 100_000; // reduce to 100k for platform threads
List<Thread> threads = new ArrayList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < taskCount; i++) {
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
}
catch (InterruptedException ignored) {}
});
threads.add(t);
t.start();
}
for (Thread t : threads) {
t.join();
}
long end = System.currentTimeMillis();
System.out.println("Platform threads completed in: " + (end - start) + " ms");
}}
Running this with 1 million threads will likely crash or freeze your system due to memory exhaustion.
Each platform thread consumes ~1MB of stack space by default, so 1M threads = ~1TB of memory.
public class VirtualThreadBenchmark {
public static void main(String[] args) throws Exception {
int taskCount = 100_000;
long start = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < taskCount; i++) {
executor.submit(() -> {
Thread.sleep(1000);
return null;
});
}
} // auto-closes and waits for all tasks
long end = System.currentTimeMillis();
System.out.println("100k Virtual threads completed in: " + (end - start) + " ms");
}
}
As the benchmark shows, 100K platform thread took just about 25 seconds to complete, while 100K virtual threads finished in just over 6 seconds — nearly four times quicker. Furthermore, to a greater extent importantly, the virtual-thread edition run with a fraction of the computer memory footprint and zero configuration change. This demonstrates exactly why virtual threads are a game-changer for I/O-heavy concurrency in Java.
Conclusion
Virtual threads don’t magically make code faster — they make concurrency radically simpler and more scalable. You can finally write straightforward, blocking-style code and still handle hundreds of thousands (or even millions) of concurrent tasks without the overhead of complex async frameworks or massive thread pools.
Try running the benchmark yourself with different workloads.