Discover the power of virtual threads in Java! Learn their benefits, differences from platform threads, and how they optimize concurrency in high-throughput applications.
What Is Process?
A process is like a container for a program running on a computer. It includes everything that program needs to run, like its memory, data, and resources. When you open software, it starts as a process. A process can have one or more threads.
What Are Threads?
Threads, on the other hand, are smaller units within a process. They’re like workers inside that process, each doing a specific job. Just as different workers in a factory handle various tasks to build something, threads handle different jobs simultaneously within a program. They share resources within the same process and work together to make the program run efficiently.
A thread is the smallest unit of processing that can be scheduled by the operating system. It runs concurrently with and largely independently of other threads. In Java, a thread is an instance of the java.lang.Thread
class.
What Are Platform Threads?
A platform thread is a thread that is implemented as a thin wrapper around an operating system (OS) thread. Platform threads are essentially a bridge between a thread in a Java program and the actual threads managed by the operating system. Each platform thread in Java corresponds to a thread managed by the underlying operating system. These threads in Java allow your code to run on specific threads provided by the operating system.
Since each platform thread in Java is tied to an actual operating system thread. This connection limits the number of available platform threads to the available operating system threads. Platform threads come with their own allocated resources, such as a substantial thread stack managed by the operating system. They enable Java code to operate within the boundaries and capabilities of the underlying operating system threads. These limitations sparked the innovation of virtual threads
The Emergence of Java Virtual Threads
Virtual threads are one of the most important innovations in Java for a long time. They were developed in Project Loom, which is an effort to simplify concurrent programming in Java by providing a new concurrency model based on lightweight threads. Virtual threads(JEP 444) were first introduced as a preview feature in JDK 19, delivered to the second preview round in JDK 20, and are now fully released as a part of JDK 21.
What Are Virtual Threads?
A virtual thread is a thread that is not tied to a specific OS thread. A virtual thread still runs code on an OS thread, but when the code calls a blocking I/O operation, the Java runtime suspends the virtual thread until it can be resumed. The OS thread associated with the suspended virtual thread is now free to perform operations for other virtual threads.
Virtual threads are implemented in a similar way to virtual memory. To simulate a lot of memory, an operating system maps a large virtual address space to a limited amount of RAM. Similarly, to simulate a lot of threads, the Java runtime maps a large number of virtual threads to a small number of OS threads.
Unlike platform threads, virtual threads typically have a shallow call stack, performing as few as a single HTTP client call or a single JDBC query. They support thread-local variables and inheritable thread-local variables, but you should carefully consider using them because a single JVM might support millions of virtual threads. Virtual threads are suitable for running tasks that spend most of the time blocked, often waiting for I/O operations to complete. However, they are not intended for long-running CPU-intensive operations.
When to Use Virtual Threads?
Use virtual threads in high-throughput concurrent applications, especially those that consist of a great number of concurrent tasks that spend much of their time waiting. Server applications are examples of high-throughput applications because they typically handle many client requests that perform blocking I/O operations such as fetching resources.
Virtual threads do not execute code faster than platform threads. Their goal is to provide scale (greater throughput) rather than speed (lower latency).
How to Create and Run a Virtual Thread?
There are two main ways to create and run a virtual thread in Java: using the Thread
and Thread.Builder
APIs, or using the Executors
class.
1. Using the Thread and Thread.Builder APIs
You can use the Thread.ofVirtual()
method to create an instance of Thread.Builder
for creating virtual threads. You can also use the Thread.Builder
methods to set the name, priority, daemon status, and uncaught exception handler of the virtual thread. Then, you can use the start
method to start the virtual thread with a Runnable
task. You can also use the Thread
methods to join, interrupt, and check the state of the virtual thread.
For example: The below code creates and starts a virtual thread that prints “Hello, world!” to the standard output.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread virtualThread = Thread.ofVirtual().name("MyThread").start(() -> {
System.out.println("Hello, world!");
});
Thread.sleep(2000);
}
}
JavaWhy Thread.sleep(2000) is mentioned in the above program?
The Java runtime does not guarantee that a virtual thread will run immediately after it is started. A virtual thread is scheduled to execute by the Java runtime as soon as possible, but it may be delayed or preempted by other threads or tasks. Therefore, the virtual thread may not have a chance to print Hello World on the console before the main thread exits.
To ensure that the virtual thread prints Hello World on the console, you need to wait for it to finish before the main thread exits. You can do this by calling the join method of the virtual thread object, which blocks the current thread until the virtual thread completes or you can even make use of Thread.sleep(ms)
method.
2. Using the Executors Class
You can use the Executors.newVirtualThreadPerTaskExecutor()
method to create an ExecutorService
that launches a new virtual thread for each task. You can also use the ExecutorService
methods to submit, execute, and manage the tasks.
For example: The below code creates and submits a task to a virtual thread executor that prints “Hello, world!” to the standard output.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// Create a virtual thread executor
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Submit a task to the executor
executor.submit(() -> {
System.out.println("Hello, world!");
});
// Shut down the executor
executor.shutdown();
}
}
}
JavaPlatform Threads vs Virtual Threads
Let’s see how Virtual Threads differ from Platform Threads
Parameter | Platform threads | Virtual threads |
---|---|---|
Creation | Created by using the Thread.ofPlatform() method or the default Thread constructor | Created by using the Thread.ofVirtual() method or the Executors.newVirtualThreadPerTaskExecutor() method |
Mapping | Directly mapped to the OS threads, and run Java code on their underlying OS threads | Not directly mapped to the OS threads, but managed and scheduled by the JVM, and run Java code on carrier threads |
Resource consumption | Consume more memory and CPU resources, because they have large stacks and context switches | Consume less memory and CPU resources, because they have small stacks and no context switches |
I/O blocking | Can be blocked by I/O operations, and waste the OS thread while waiting | Can be suspended and resumed by the JVM when they perform blocking I/O operations, and free the OS thread to run other virtual threads |
APIs | Require using non-blocking or asynchronous APIs to avoid blocking and improve performance, which can complicate the code | Can use blocking or synchronous APIs without affecting performance, and simplify the code |
Task suitability | Suitable for running any kind of task, but they are a limited resource | Not suitable for running long-running CPU-intensive tasks, because they can starve other virtual threads on the same carrier thread |
Native methods | Can call native methods that perform blocking or non-blocking operations | Cannot call native methods that perform blocking operations, because they can block the carrier thread |
How to Determine if a Thread is a Virtual Thread?
To determine if a thread is a virtual thread in Java, you can utilize the isVirtual()
method from the Thread
class. This method returns a boolean value, signaling whether the thread is a virtual thread or a platform thread. This method will yield true
for virtual threads and false
for platform threads.
For example, the following code creates and starts a virtual thread and a platform thread, and checks their types using the isVirtual()
method:
Thread virtualThread = Thread.ofVirtual().start(() -> {
System.out.println("Hello, world!");
});
Thread platformThread = Thread.ofPlatform().start(() -> {
System.out.println("Hello, world!");
});
System.out.println("\nIs virtualThread a virtual thread? " + virtualThread.isVirtual());
System.out.println("Is platformThread a virtual thread? " + platformThread.isVirtual());
JavaThe output of the code is:
Hello, world!
Hello, world!
Is virtualThread a virtual thread? true
Is platformThread a virtual thread? false
JavaStarted vs Unstarted Virtual Threads
When you create a virtual thread in Java, you can choose to start it immediately or leave it unstarted. A started virtual thread is scheduled to execute by the Java runtime as soon as possible. An unstarted virtual thread is not scheduled to execute until you explicitly invoke its start
method.
You can use the Thread
and Thread.Builder
APIs to create and start a virtual thread with a Runnable
task. For example, the following code creates and starts a virtual thread that prints “Hello, world!” to the standard output:
Thread virtualThread = Thread.ofVirtual().start(() -> {
System.out.println("Hello, world!");
});
JavaYou can also use the Thread.ofVirtual().unstarted()
method to create an unstarted virtual thread with a Runnable
task. For example, the following code creates an unstarted virtual thread that prints “Hello, world!” to the standard output:
Thread virtualThread = Thread.ofVirtual().unstarted(() -> {
System.out.println("Hello, world!");
});
JavaTo start an unstarted virtual thread, you need to call its start
method. For example, the following code starts the unstarted virtual thread created above:
virtualThread.start();
JavaYou can also use the Thread.startVirtualThread
method to create and start a virtual thread with a Runnable
task in one step. For example, the following code creates and starts a virtual thread that prints “Hello, world!” to the standard output:
Thread virtualThread = Thread.startVirtualThread(() -> {
System.out.println("Hello, world!");
});
JavaJava Virtual Threads in Action
Example 1: Creating and running 10,000 threads
This program creates and runs 10,000 threads that print “Hello, world!” to the standard output. It measures the time and memory used by the program.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) throws InterruptedException {
// Creating a thread pool with virtual threads
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
long startMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
System.out.println("Hello, world!");
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
long endTime = System.currentTimeMillis();
long endMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
long time = endTime - startTime;
long memory = endMemory - startMemory;
System.out.println("Time: " + time + " ms");
System.out.println("Memory: " + memory + " bytes");
}
}
}
JavaIf you run this program with platform threads, you will likely get an OutOfMemoryError
or a very slow performance, because creating 10,000 OS threads is very expensive and resource-intensive.
If you run this program with virtual threads, you will get a much faster and lighter performance, because creating 10,000 virtual threads is very cheap and efficient. The Java runtime will use a small number of OS threads to execute the virtual threads.
Example 2: Adaptable Thread Behavior – Unbound from Specific OS Threads
The program demonstrates the use of virtual threads in Java, which are lightweight threads that are not bound to a specific OS thread. A virtual thread runs code on an OS thread, but when it encounters a blocking I/O operation, such as Thread.sleep(), the Java runtime suspends the virtual thread and frees the OS thread for other virtual threads. The ForkJoinPool assigns a different OS thread to the virtual thread when it is ready to run again.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread virtualThread = Thread.ofVirtual().start(() -> {
System.out.println("Virtual thread started: " + Thread.currentThread());
// Simulate a long-running task with multiple blocking points
for (int i = 0; i < 15; i++) {
System.out.println("Virtual thread working (iteration " + i + "): " + Thread.currentThread());
try {
Thread.sleep(1000); // Simulate 1-second blocking operation
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Re-interrupt if needed
}
}
System.out.println("Task completed on virtual thread: " + Thread.currentThread());
});
System.out.println("Main thread continuing...");
System.out.println("Main thread waiting for virtual thread...");
virtualThread.join(); // Wait for virtual thread completion
System.out.println("Virtual thread finished.");
}
}
JavaThe output shows that the ForkJoinPool assigned two different worker threads to the virtual thread during its execution. This illustrates the flexibility and scalability of virtual threads.
What Is Virtual Thread Pinning?
Virtual thread pinning is a situation where a virtual thread cannot be detached from its underlying OS thread, also known as the carrier thread. This means that the virtual thread occupies the carrier thread for its entire lifetime, preventing other virtual threads from using it. This reduces the scalability and performance benefits of virtual threads.
Virtual thread pinning can happen for several reasons such as:
- Synchronized Blocks or Methods: When a virtual thread enters a synchronized block or method, it gets attached or locked to its carrier thread. This means the carrier thread can’t be used for other tasks while this block or method is running.
- Native Methods or Foreign Functions: If a virtual thread runs a native method or a foreign function, it also gets locked or pinned to that operation.
Detecting Virtual Thread Pinning
To detect and diagnose virtual thread pinning, you can use the -Djdk.tracePinnedThreads=full
or -Djdk.tracePinnedThreads=short
JVM options. These options enable the logging of pinning events. The full option prints more details than the short option, such as the stack trace.
You can specify these options as system properties in your code, or as command-line arguments when launching the JVM, such as java -Djdk.tracePinnedThreads=full.
For example, the below code showcases virtual thread pinning and detection wherein a synchronized task runs within a virtual thread, and the virtual thread gets pinned to its carrier thread.
public class Main {
public synchronized void synchronizedTask() {
try {
Thread.sleep(100000);
System.out.println("Work done by virtual thread. isVirtual: " + Thread.currentThread().isVirtual());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws InterruptedException {
//System.setProperty("jdk.tracePinnedThreads", "full");
Main main = new Main();
Thread.startVirtualThread(main::synchronizedTask).join();
}
}
JavaOutput: -Djdk.tracePinnedThreads=full & -Djdk.tracePinnedThreads=short
javac Main.java
Java -Djdk.tracePinnedThreads=full Main
Thread[#21,ForkJoinPool-1-worker-1,5,CarrierThreads]
java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183)
java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)
java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:793)
java.base/java.lang.Thread.sleep(Thread.java:507)
Main.synchronizedTask(Main.java:5) <== monitors:1
java.base/java.lang.VirtualThread.run(VirtualThread.java:309)
Work done by virtual thread. isVirtual: true
Pinning Detection = Fulljavac Main.java
Java -Djdk.tracePinnedThreads=short Main
Thread[#21,ForkJoinPool-1-worker-1,5,CarrierThreads]
Main.synchronizedTask(Main.java:5) <== monitors:1
Work done by virtual thread. isVirtual: true
Pinning Detection = ShortMystery of Empty Thread Names in Virtual Threads
Discover why virtual thread names often appear blank in logs or when using Thread.currentThread().getName()
:
Absence of Default Names:
Virtual threads, unlike conventional OS threads, lack predefined names from the system. This aligns with their lightweight design focused on rapid creation and management. Consequently, calling getName()
on a virtual thread without an explicitly set name returns an empty string.
Setting Names Explicitly:
To assign a name to a virtual thread, utilize the setName()
method or the Thread.builder().name()
method. This can be useful for debugging or logging purposes, but you should not rely on the thread name for any logic or functionality in your code. You can use the Thread.isVirtual()
method to check if the current thread is a virtual thread or a platform thread or if you need a unique identifier for a virtual thread, use its thread ID like : Thread.currentThread().threadId()
.
Virtual Threads vs Reactive Programming
Virtual threads and reactive programming are two different approaches to handle concurrency and scalability in Java applications. They both aim to reduce the overhead of blocking I/O operations and improve the utilization of system resources. However, they have different advantages and disadvantages, depending on the use case and the programming model. Here is a table that compares some of the key aspects of virtual threads and reactive programming:
Parameter | Virtual Threads | Reactive Programming |
---|---|---|
Concept | Lightweight threads that are not bound to a specific OS thread and can be suspended and resumed by the Java runtime when they perform blocking I/O operations. | A paradigm that uses asynchronous, non-blocking, and event-driven streams of data to handle the complexity of concurrent applications. |
Programming Model | Imperative, sequential, and familiar to most Java developers. | Functional, declarative, and requires a learning curve and a mindset shift. |
Programming Style | You write code as usual, using blocking APIs and libraries, and the JVM takes care of suspending and resuming the virtual threads when they are blocked. | You write code using reactive APIs and libraries, such as WebFlux, that return data streams, such as Flux and Mono, that you can compose and transform. |
Advantages | Simplifies the development and maintenance of concurrent applications, reduces the memory footprint and increases the throughput of I/O-intensive applications, leverages existing blocking APIs and libraries. | Provides fine-grained control and flexibility over concurrency and backpressure, enables composition and transformation of data streams, integrates with reactive frameworks and libraries. |
Disadvantages | Not suitable for long-running CPU-intensive operations, may encounter compatibility issues with native methods or thread-local variables. | Increases the complexity and verbosity of the code, requires careful error handling and testing, may encounter performance issues with blocking APIs or libraries |
Things to Consider
Here are some things to consider when working with Java Virtual Threads:
- No Default Names for Virtual Threads: Virtual threads lack default names. Assign names explicitly using
setName()
for clearer logs and easier debugging. - Thread IDs for Identification: Utilize the unique thread ID (
Thread.currentThread().threadId()
) for reliable identification when needed. - JVM Compatibility: Ensure Java 19 or later for utilizing virtual threads effectively.
- Library and Framework Compatibility: Check the compatibility of third-party libraries and frameworks with virtual threads for seamless integration.
- Debugging and Profiling Consideration: Adapt debugging and profiling tools and techniques to work efficiently with virtual threads.
- Select Task Type Suitably: Employ virtual threads for I/O-bound tasks and consider traditional threads for CPU-intensive workloads.
- Experiment and Stay Updated: Virtual threads are evolving. Stay updated and experiment to uncover their optimal use cases in your applications.
FAQs
How many virtual threads can be created?
There is no fixed limit on the number of virtual threads that can be created, as they are not bound to a specific OS thread. However, the actual number of virtual threads that can be created depends on the available memory, application workload and the configuration of the JVM.
What are the benefits of virtual threads?
Virtual threads can reduce the memory footprint and increase the throughput of I/O-intensive applications, as they can run on a small number of OS threads without occupying them for their entire lifetime. They also simplify the development and maintenance of concurrent applications, as they leverage existing blocking APIs and libraries.
What are the limitations of virtual threads?
Virtual threads are not suitable for long-running CPU-intensive tasks, as they may cause performance issues or starvation of other virtual threads. They may also encounter compatibility issues with some native methods or libraries that do not support them, or with thread-local or inheritable thread-local variables.
Conclusion
The journey from processes to threads and the emergence of Java Virtual Threads showcases a remarkable evolution in task management within Java. The virtual threads revolutionize concurrency by providing a lightweight, scalable alternative, detached from specific OS threads. Their versatility and power promise a significant impact on Java’s concurrency landscape, shaping the way we approach programming in the future.
Learn More
Interested in learning more?
Check out our blog on gracefully handling UnsupportedOperationException in Java
Top Picks for Learning Java
Explore the recommended Java books tailored for learners at different levels, from beginners to advanced programmers.
Disclaimer: The products featured or recommended on this site are affiliated. If you purchase these products through the provided links, I may earn a commission at no additional cost to you.
- All Levels Covered: Designed for novice, intermediate, and professional programmers alike
- Accessible Source Code: Source code for all examples and projects are available for download
- Clear Writing Style: Written in the clear, uncompromising style Herb Schildt is famous for
Head First Java: A Brain-Friendly Guide
- Engaging Learning: It uses a fun approach to teach Java and object-oriented programming.
- Comprehensive Content: Covers Java's basics and advanced topics like lambdas and GUIs.
- Interactive Learning: The book's visuals and engaging style make learning Java more enjoyable.
Modern Java in Action: Lambdas, streams, functional and reactive programming
- Latest Java Features: Explores modern Java functionalities from version 8 and beyond, like streams, modules, and concurrency.
- Real-world Applications: Demonstrates how to use these new features practically, enhancing understanding and coding skills.
- Developer-Friendly: Tailored for Java developers already familiar with core Java, making it accessible for advancing their expertise.
- Java Essentials: Learn fundamental Java programming through easy tutorials and practical tips in the latest edition of the For Dummies series.
- Programming Basics: Gain control over program flow, master classes, objects, and methods, and explore functional programming features.
- Updated Coverage: Covers Java 17, the latest long-term support release, including the new 'switch' statement syntax, making it perfect for beginners or those wanting to brush up their skills.
Add a Comment