Virtual Threads are lightweight, user-mode threads introduced to the Java platform through Project Loom. Unlike traditional Java threads, managed directly by the operating system (OS), these threads are managed by the Java runtime. This shift allows for handling a much larger number of concurrent threads efficiently. It opens the door to vastly improved scalability and performance in Java applications.
Before we delve into virtual threads, it’s important to understand the current state of threading in Java.
Traditional Threads
Java’s concurrency model has historically been based on the concept of OS-level threads (often referred to as “native” or “platform” threads). These threads are managed by the operating system and mapped directly to the hardware threads (CPU cores) available.
Traditional threads architecture
In previous versions of the Java Virtual Machine (JVM), only one type of thread existed, known as a “classic” or “platform” thread. Specifically, each time a platform thread was created, it corresponded directly to a thread managed by the operating system. As a result, this 1:1 mapping meant that an operating system thread was dedicated exclusively to the JVM’s platform thread for its entire lifecycle. Consequently, this tight coupling limited the number of threads the JVM could efficiently manage. Consequently, the operating system thread remained occupied until the platform thread terminated, preventing it from being assigned to any other task.

Key Characteristics of Traditional Threads:
Heavyweight: Each thread consumes a significant amount of memory, primarily for its stack. This overhead limits the number of threads that can be efficiently created and managed.
Context Switching: Switching between threads incurs a non-trivial performance cost because the OS needs to save and load thread states.
Limited Scalability: High memory usage and context-switching overheads make it impractical to manage millions of threads, which is a bottleneck for applications with massive concurrency requirements.
Thread Pools and Executors
To mitigate the cost of creating and destroying threads, Java provides thread pools through the java.util.concurrent package. Thread pools reuse a fixed number of threads to execute tasks, reducing the overhead associated with thread lifecycle management.
While thread pools help, they do not solve the core limitations related to the high cost and memory footprint of traditional threads, especially when scaling to large numbers of concurrent tasks.
Virtual Threads
Virtual threads are introduced in Project Loom to address the limitations of traditional threading. They aim to provide a lightweight, efficient threading model that can support millions of concurrent threads with minimal overhead.
These threads managed by the Java runtime rather than the OS. They are designed to be much lighter in terms of memory and system resource consumption compared to traditional threads.
Architecture

Fig: OS Threads are not allocated to Platform threads until real work needs to be executed

Fig: Virtual Threads are mapped to platform threads when it does real work
To optimize the use of underlying operating system threads, JDK 19 introduced virtual threads. In this new architecture, a virtual thread is assigned to a platform thread (also known as a carrier thread) only when it is performing actual work. According to the virtual thread’s lifecycle, it only gets linked to a platform thread during specific stages of execution, namely steps #3 and #5. During these stages, the platform thread, and consequently the operating system thread, is utilized for execution. At all other times, these threads exist as objects in the Java heap memory, similar to other application objects. This design makes virtual threads lightweight and more efficient compared to the traditional approach.
Key Characteristics
Lightweight: These threads have a significantly smaller memory footprint than traditional threads. This makes it feasible to create millions of virtual threads without exhausting system resources.
Efficient Scheduling: These are managed and scheduled by the Java runtime, allowing more efficient and granular control over execution.
Reduced Context Switching Costs: Because the JVM manages virtual threads, context switching can be optimized, reducing the overhead compared to OS-managed threads.
How It Works
Decoupling the application’s threads from OS threads, they operate under the hood by utilizing lightweight, user-mode management, allowing efficient handling and scalability.
User-Space Scheduling: The Java runtime (JVM) schedules virtual threads in user space, avoiding the heavy involvement of the OS scheduler. This approach minimizes the cost of context switching.
Stack Management: Threads use a dynamic stack that grows and shrinks as needed, unlike traditional threads that allocate a large, fixed-size stack upfront.
Blocking and Non-blocking Operations: When a virtual thread blocks (e.g., waits for I/O), the JVM can efficiently park the thread and utilize the underlying OS thread for other tasks, effectively decoupling the blocking operation from the thread’s lifecycle.
Example: Creating Virtual Threads
Creating and managing virtual threads in Java is straightforward and integrates seamlessly with existing code. Here’s a basic example:
public class VirtualThreadExample {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello from virtual thread: " + Thread.currentThread());
};
Thread virtualThread = Thread.ofVirtual().start(task);
}
}
In this example, Thread.ofVirtual().start(task) creates and starts a virtual thread to execute the provided task.
Benefits of Virtual Threads
They bring several significant advantages over traditional threading models:
- Scalability: With their low overhead, virtual threads allow applications to scale to millions of concurrent tasks without running into resource constraints.
- Simplified Concurrency: Developers can use virtual threads without worrying about thread management complexities, simplifying the design of concurrent applications.
- Better Resource Utilization: They enable more efficient use of CPU and memory resources, leading to better performance, especially in I/O-bound applications.
- Integration with Existing APIs: These are designed to work with existing Java APIs, making it easier to adopt them without substantial code changes.
Practical Insights and Adoption
To start using virtual threads, you need a JDK that includes the Project Loom features. As of JDK 19, virtual threads are available as a preview feature. Here’s how you can experiment with virtual threads in your existing Java applications:
Step-by-Step Guide
- Download and Install JDK 19 or Higher: Ensure you have a compatible JDK version that supports virtual threads.
- Enable Preview Features: When compiling and running your application, enable the preview features using the
--enable-previewflag. - Integrate Virtual Threads: Replace your traditional threading code with virtual thread constructs where applicable.
- Test and Benchmark: Evaluate the performance and behavior of your application with virtual threads to understand their impact on scalability and resource usage.
Conclusion
Virtual threads represent a significant leap forward in Java’s concurrency capabilities. By providing a lightweight, scalable, and efficient threading model, they open up new possibilities for building high-performance, concurrent applications.