Virtual Threads: A Game-Changer for Concurrency
In this blog, we'll explore Java Virtual Threads, compare them to traditional platform threads, and provide example code to highlight the differences.
Join the DZone community and get the full member experience.
Join For FreeDespite being nearly 30 years old, the Java platform remains consistently among the top three most popular programming languages. This enduring popularity can be attributed to the Java Virtual Machine (JVM), which abstracts complexities such as memory management and compiles code during execution, enabling unparalleled internet-level scalability.
Java's sustained relevance is also due to the rapid evolution of the language, its libraries, and the JVM. Java Virtual Threads, introduced in Project Loom, which is an initiative by the OpenJDK community, represent a groundbreaking change in how Java handles concurrency.
The Complete Java Coder Bundle.*
*Affiliate link. See Terms of Use.
Exploring the Fabric: Unveiling Threads
A thread is the smallest schedulable unit of processing, running concurrently and largely independently of other units. It's an instance of java.lang.Thread
. There are two types of threads: platform threads and virtual threads.
A platform thread is a thin wrapper around an operating system (OS) thread, running Java code on its underlying OS thread for its entire lifetime. Consequently, the number of platform threads is limited by the number of OS threads. These threads have large stacks and other OS-managed resources, making them suitable for all task types but potentially limited in number.
Virtual threads in Java, unlike platform threads, aren't tied to specific OS threads but still execute on them. When a virtual thread encounters a blocking I/O operation, it pauses, allowing the OS thread to handle other tasks. Similar to virtual memory, where a large virtual address space maps to limited RAM, Java's virtual threads map many virtual threads to fewer OS threads. They're ideal for tasks with frequent I/O waits but not for sustained CPU-intensive operations. Hence virtual threads are lightweight threads that simplify the development, maintenance, and debugging of high-throughput concurrent applications.
Comparing the Threads of Fabric: Virtual vs. Platform
Let’s compare platform threads with virtual threads to understand their differences better.
Crafting Virtual Threads
Creating Virtual Threads Using Thread Class and Thread.Builder Interface
The example below creates and starts a virtual thread that prints a message. It uses the join method to ensure the virtual thread completes before the main thread terminates, allowing you to see the printed message.
Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello World!! I am Virtual Thread"));
thread.join();
The Thread.Builder
interface allows you to create threads with common properties like thread names. The Thread.Builder.OfPlatform
subinterface creates platform threads, while Thread.Builder.OfVirtual
creates virtual threads.
Here’s an example of creating a virtual thread named "MyVirtualThread" using the Thread.Builder
interface:
Thread.Builder builder = Thread.ofVirtual().name("MyVirtualThread");
Runnable task = () -> {
System.out.println("Thread running");
};
Thread t = builder.start(task);
System.out.println("Thread name is: " + t.getName());
t.join();
Creating and Running a Virtual Thread Using Executors.newVirtualThreadPerTaskExecutor()
Method
Executors allow you to decouple thread management and creation from the rest of your application.
In the example below, an ExecutorService is created using the Executors.newVirtualThreadPerTaskExecutor()
method. Each time ExecutorService.submit(Runnable)
is called, a new virtual thread is created and started to execute the task. This method returns a Future instance. It's important to note that the Future.get()
method waits for the task in the thread to finish. As a result, this example prints a message once the virtual thread's task is completed.
try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<?> future = myExecutor.submit(() -> System.out.println("Running thread"));
future.get();
System.out.println("Task completed");
// ...
Is Your Fabric Lightweight With Virtual Threads?
Memory
Program 1: Create 10,000 Platform Threads
public class PlatformThreadMemoryAnalyzer {
private static class MyTask implements Runnable {
@Override
public void run() {
try {
// Sleep for 10 minutes
Thread.sleep(600000);
} catch (InterruptedException e) {
System.err.println("Interrupted Exception!!");
}
}
}
public static void main(String args[]) throws Exception {
// Create 10000 platform threads
int i = 0;
while (i < 10000) {
Thread myThread = new Thread(new MyTask());
myThread.start();
i++;
}
Thread.sleep(600000);
}
}
Program 2: Create 10,000 Virtual Threads
public class VirtualThreadMemoryAnalyzer {
private static class MyTask implements Runnable {
@Override
public void run() {
try {
// Sleep for 10 minutes
Thread.sleep(600000);
} catch (InterruptedException e) {
System.err.println("Interrupted Exception!!");
}
}
}
public static void main(String args[]) throws Exception {
// Create 10000 virtual threads
int i = 0;
while (i < 10000) {
Thread.ofVirtual().start(new Task());
i++;
}
Thread.sleep(600000);
}
}
Executed both programs simultaneously in a RedHat VM. Configured the thread stack size to be 1mb (by passing JVM argument -Xss1m). This argument indicates that every thread in this application should be allocated 1mb of stack size. Below is the top command output of the threads running.
You can notice that the virtual threads only occupies 7.8mb (i.e., 7842364 bytes), whereas the platform threads program occupies 19.2gb. This clearly indicates that virtual threads consume comparatively much less memory.
Thread Creation Time
Program 1: Launches 10,000 platform threads
public class PlatformThreadCreationTimeAnalyzer {
private static class Task implements Runnable {
@Override
public void run() {
System.out.println("Hello! I am a Platform Thread");
}
}
public static void main(String[] args) throws Exception {
long startTime = System.currentTimeMillis();
for (int counter = 0; counter < 10_000; ++counter) {
new Thread(new Task()).start();
}
System.out.print("Platform Thread Creation Time: " + (System.currentTimeMillis() - startTime));
}
}
Program 2: Launches 10,000 virtual threads
public class VirtualThreadCreationTimeAnalyzer {
private static class Task implements Runnable {
@Override
public void run() {
System.out.println("Hello! I am a Virtual Thread");
}
}
public static void main(String[] args) throws Exception {
long startTime = System.currentTimeMillis();
for (int counter = 0; counter < 10_000; ++counter) {
Thread.startVirtualThread(new Task());
}
System.out.print("Virtual Thread Creation Time: " + (System.currentTimeMillis() - startTime));
}
}
Below is the table that summarizes the execution time of these two programs:
Virtual Threads |
Platform Threads |
|
Execution Time |
84 ms |
346 ms |
You can see that the virtual Thread took only 84 ms to complete, whereas the Platform Thread took almost 346 ms. It’s because platform threads are more expensive to create. Because whenever a platform needs to be created an operating system thread needs to be allocated to it. Creating and allocating an operating system thread is not a cheap operation.
Reweaving the Fabric: Applications of Virtual Threads
Virtual threads can significantly benefit various types of applications, especially those requiring high concurrency and efficient resource management. Here are a few examples:
- Web servers: Handling a large number of simultaneous HTTP requests can be efficiently managed with virtual threads, reducing the overhead and complexity of traditional thread pools.
- Microservices: Microservices often involve a lot of I/O operations, such as database queries and network calls. Virtual threads can handle these operations more efficiently.
- Data processing: Applications that process large amounts of data concurrently can benefit from the scalability of virtual threads, improving throughput and performance.
Weaving Success: Avoiding Pitfalls
To make the most out of virtual threads, consider the following best practices:
- Avoid synchronized blocks/methods: When using virtual threads with synchronized blocks, they may not relinquish control of the underlying OS thread when blocked, limiting their benefits.
- Avoid thread pools for virtual threads: Virtual threads are meant to be used without traditional thread pools. The JVM manages them efficiently, and thread pools can introduce unnecessary complexity.
- Reduce ThreadLocal usage: Millions of virtual threads with individual ThreadLocal variables can rapidly consume Java heap memory.
Wrapping It Up
Virtual threads in Java are threads implemented by the Java runtime, not the operating system. Unlike traditional platform threads, virtual threads can scale to a high number — potentially millions — within the same Java process. This scalability allows them to efficiently handle server applications designed in a thread-per-request style, improving concurrency, throughput, and hardware utilization.
Developers familiar with java.lang.Thread since Java SE 1.0 can easily use virtual threads, as they follow the same programming model. However, practices developed to manage the high cost of platform threads are often counterproductive with virtual threads, requiring developers to adjust their approach. This shift in thread management encourages a new perspective on concurrency.
"Hello, world? Hold on, I’ll put you on hold, spawn a few more threads, and get back to you"
Opinions expressed by DZone contributors are their own.
Comments