Project Loom: A Deep Dive into Virtual Threads
1. Why Now ?
It’s been a long time since I seriously studied Java threads. So why am I writing about them now?
Well… I blame my three colleagues. 😄 During one of our discussions, they brought up the topic of threads regarding joins. That conversation sparked my curiosity, and while exploring it further, I stumbled upon Project Loom. The more I read about it, the more fascinating it became — and that’s exactly why you’re reading this blog today.
2. Introduction: Why Do We Need Virtual Threads?
Although Project Loom started as an OpenJDK research project back in 2017, it truly caught everyone’s attention when Virtual Threads were introduced as a preview in Java 19 and later became a production-ready feature in Java 21. Since then, it has been one of the hottest topics in the Java community.
For years, building highly concurrent applications meant choosing between traditional threads, which consume significant system resources, or reactive programming, which offers scalability but often increases code complexity.
Project Loom changes that equation. It allows developers to write simple, synchronous code while handling an enormous number of concurrent tasks using lightweight Virtual Threads.
So, let’s explore why Project Loom is considered one of the biggest changes to Java concurrency in recent years and see what makes Virtual Threads so special.
3. The Evolution of Java Concurrency
Before jumping into Project Loom, let’s go back a little.
Let’s assume you’re writing your very first Java program, and you create just one thread. Everything works perfectly.
Now imagine the same application is deployed in a large enterprise.One user opens the application — it creates one thread.At the same time, two more users access it — now there are three threads.
Then 100 users…
Then 1,000…
Then 10,000…
Then 100,000…
Every request creates a new thread.
It started with just one thread, but as the number of users keeps increasing, the number of threads grows just as fast. Managing 100,000 threads isn’t easy — it’s like having 100,000 managers asking you to do different tasks at the same time.
The reason is simple: every Platform Thread is backed by an Operating System (OS) thread. Creating a few of them is perfectly fine, but creating thousands or even hundreds of thousands consumes a lot of memory, increases context switching, and eventually affects the application’s performance.
To overcome this, developers came up with clever solutions such as Thread Pools, CompletableFuture, and later Reactive Programming using frameworks like Spring WebFlux and RxJava. These approaches solved the scalability problem, but they also made our applications more difficult to write, understand, and debug.
It almost felt like we had to choose one:
✔️ Simple code → Limited scalability
or
✔️ Highly scalable application → Increased complexity
For years, there wasn’t a way to have both.
Then Project Loom entered the picture…
4. Understanding Platform Threads
We have been talking about Platform Threads all this time, but what exactly are they?
Simply put, a Platform Thread is the traditional thread that Java has been using for years. Whenever you create a thread using new Thread(), Java asks the Operating System to create a thread as well.
So if your application creates:
- 1 Java Thread → 1 OS Thread
- 100 Java Threads → 100 OS Threads
- 10,000 Java Threads → 10,000 OS Threads
Simple, right?
The problem starts when the numbers keep growing.
Operating System threads are expensive. Each one requires its own memory and system resources. As more users access your application, more Platform Threads are created, increasing memory usage and context switching. Eventually, this becomes a bottleneck for highly concurrent applications.
This 1:1 relationship between a Java thread and an Operating System thread is exactly the limitation that Project Loom tries to overcome.
5. What Are Virtual Threads?
I wondered…
“If creating thousands of Platform Threads is expensive, how can Java suddenly say we can create millions of threads?”
The answer is Virtual Threads.
Unlike Platform Threads, a Virtual Thread is not directly tied to an Operating System thread. Instead, it is managed by the Java Virtual Machine (JVM) itself.
Think of it like this.
Imagine? No need to imagine — it’s the reality of every developer.
You’re working on a feature, but it gets blocked. Do you sit idle? Of course not. Your team lead assigns you another task, and once the blocker is resolved, you come back and continue your original work.
That’s exactly how Virtual Threads work.
Whenever a Virtual Thread encounters a blocking operation — like waiting for a database query, an HTTP request, or even Thread.sleep()—the JVM temporarily pauses that Virtual Thread and frees the underlying Operating System thread to perform other work.
Once the blocking operation completes, the Virtual Thread simply resumes where it left off.
This means thousands or even millions of Virtual Threads can efficiently share a relatively small number of Operating System threads, making Java applications far more scalable without changing the way we write code.
6. Platform Threads vs Virtual Threads
Now that we know what Virtual Threads are, let’s see how different they are from traditional Platform Threads.
A Platform Thread is directly mapped to an Operating System thread (1:1 mapping). Every time you create a new thread, the operating system also creates a new thread for it.
A Virtual Thread, however, is managed by the JVM. Thousands of Virtual Threads can efficiently share a small number of Operating System threads, making them much lighter and more scalable.
7. The Practical Problem
We’ve learned that Platform Threads are expensive, but how expensive are they in a real application?
Let’s assume your Spring Boot application suddenly receives 10,000 requests at the same time. Every request needs to call a database or an external API, and each one creates a Platform Thread. Most of these threads spend their time simply waiting for a response while still occupying Operating System resources.
Instead of just talking about it, let’s recreate this scenario with a simple Java program and see what actually happens.
8. Traditional Implementation (Platform Threads)
Let’s simulate a real-world scenario.
We’ll create 10,000 tasks, where each task waits for 1 second to mimic a slow database query or an external API call. Since we’re using Platform Threads, every task requires an Operating System thread.
Get VStackStories’s stories in your inbox
Join Medium for free to get updates from this writer.
Let’s see how the JVM handles this workload.
9. The Virtual Thread Solution
The previous example created thousands of Platform Threads. Now, let’s change just one line of code and use Virtual Threads instead.
The business logic remains exactly the same. The only difference is the executor we use.
Let’s see how Virtual Threads handle the same 10,000 tasks.
10. What Just Happened?
At first glance, both programs look almost identical. In fact, the business logic didn’t change at all — we only changed the executor.
The difference is hidden behind the scenes.
With Platform Threads, every task is backed by its own Operating System thread. In the Virtual Thread version, the JVM creates lightweight Virtual Threads and efficiently schedules them over a small number of Operating System threads.
This is why Virtual Threads can handle thousands — or even millions — of concurrent tasks while using far fewer system resources.
But how does the JVM achieve this?
Let’s look under the hood.
11. How Virtual Threads Work Internally
So, how can a small number of Operating System threads handle thousands of Virtual Threads?
The secret lies in Carrier Threads.
When a Virtual Thread starts, it is mounted on a Carrier Thread (an Operating System thread) and begins execution.
If the Virtual Thread encounters a blocking operation — such as a database query, an HTTP call, or even Thread.sleep()—the JVM parks the Virtual Thread. This frees the Carrier Thread immediately, allowing it to execute another Virtual Thread instead of sitting idle.
Once the blocking operation completes, the JVM unparks the Virtual Thread and continues its execution from exactly where it stopped.
That’s the magic behind Project Loom.
12. Carrier Threads & Continuations
A Carrier Thread is simply a normal Operating System thread. The JVM uses these threads to execute Virtual Threads.
When a Virtual Thread needs to wait for an I/O operation, the JVM saves its current state using a mechanism called a Continuation. The Virtual Thread is paused, the Carrier Thread is released to do other work, and once the operation completes, the Continuation restores the Virtual Thread and execution resumes from where it stopped.
Because of this mechanism, a small number of Carrier Threads can efficiently execute thousands of Virtual Threads.
13. Best Practices & Things to Avoid
Virtual Threads are powerful, but they’re not a replacement for every concurrency problem.
Here are a few things to keep in mind:
- ✅ Use Virtual Threads for I/O-bound tasks like database queries, REST API calls, and file operations.
- ✅ Create a new Virtual Thread for each task. They are lightweight, so there’s no need to pool them.
- ❌ Avoid long-running CPU-intensive tasks. Platform Threads are still a better choice for heavy computations.
- ❌ Be careful with
synchronizedblocks. If a Virtual Thread blocks inside a synchronized section, it can pin the Carrier Thread and reduce scalability.
14. Thread Pinning
Virtual Threads are lightweight, but there’s one thing you should be careful about — Thread Pinning.
If a Virtual Thread enters a synchronized block and performs a blocking operation, the Carrier Thread cannot be released. Instead of executing another Virtual Thread, it remains blocked until the operation completes.
This reduces the scalability benefits of Virtual Threads.
Whenever possible, prefer modern locking mechanisms like ReentrantLock over long-running synchronized blocks.
15. When Should You Use Virtual Threads?
Virtual Threads are a great choice for applications that spend most of their time waiting.
Use them for:
- REST APIs
- Database operations
- External API calls
- File I/O
- Microservices
For CPU-intensive workloads such as image processing, machine learning, or heavy mathematical computations, Platform Threads are still the better choice.
16. Conclusion
Project Loom isn’t just another Java feature — it’s a new way of thinking about concurrency.
For years, developers had to choose between simple code and highly scalable applications. With Virtual Threads, that trade-off is much smaller. We can continue writing familiar synchronous code while handling thousands, or even millions, of concurrent tasks efficiently.
Of course, Virtual Threads aren’t a silver bullet. Understanding concepts like Carrier Threads, Continuations, and Thread Pinning helps you use them effectively.
If you’re starting a new project on Java 21+, it’s definitely worth exploring Virtual Threads. They simplify concurrent programming while keeping your code clean, readable, and scalable.
I hope this blog helped you understand Project Loom a little better. If you enjoyed it or have questions, feel free to share your thoughts. See you in the next blog! 🚀








