Understanding Virtual Threads in Java 21
With Java 21, Virtual Threads introduce a new way to handle concurrency. They are lightweight, scalable, and allow developers to write simpler, more intuitive code.
What is a Virtual Thread?
A Virtual Thread is a lightweight thread managed entirely by the Java Virtual Machine (JVM), rather than the operating system. Unlike traditional threads, Virtual Threads are not tied to an OS thread, making them extremely resource-efficient. They allow developers to write straightforward blocking code that scales as efficiently as asynchronous programming. With Virtual Threads, millions of tasks can run concurrently, making them ideal for modern, high-concurrency applications.
Traditional Threads vs. Virtual Threads
Traditional threads are backed by OS threads, making them heavy and difficult to scale. Creating thousands or millions of threads often leads to performance bottlenecks. This complexity has driven developers toward asynchronous programming, which, while efficient, resulted in hard-to-read code.
Virtual Threads solve these problems by being:
* Managed by the JVM, not the OS.
* Capable of handling millions of tasks concurrently.
* Allowing developers to write blocking code that performs like asynchronous code.
With the introduction of Virtual Threads in Java 19 (as a preview) and Java 21 (as a stable feature), new APIs have been added to simplify creating both Platform Threads (traditional OS-managed threads) and Virtual Threads.
Creating a Platform Thread
Platform threads are the traditional threads managed by the operating system. The new Thread.ofPlatform() API provides a simple way to create them.
public class PlatformThreadExample {
public static void main(String[] args) {
Thread.ofPlatform().start(() -> System.out.println("Running in a Platform Thread: " + Thread.currentThread()));
}
}
Output:
Running in a Platform Thread: Thread[#21,Thread-0,5,main]
Creating a Virtual Thread
Virtual threads are lightweight threads managed by the JVM, designed for high-concurrency applications.
public class VirtualThreadExample {
public static void main(String[] args) {
Thread virtualThread = Thread.ofVirtual().start(() -> {
System.out.println("Running in a Virtual Thread: " + Thread.currentThread());
});
// Wait for the thread to complete (optional)
try {
virtualThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Thread interrupted: " + e.getMessage());
}
}
}
Output:
Running in a Virtual Thread: VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
How Virtual Threads Work
Virtual Threads are decoupled from OS threads. When a Virtual Thread encounters a blocking operation, the JVM pauses the thread and releases the underlying OS thread for other tasks. Once the operation is complete, the Virtual Thread resumes.
import java.util.concurrent.Executors;
public class VirtualThreadsDemo {
public static void main(String[] args) throws InterruptedException {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 1; i <= 5; i++) {
int threadNumber = i;
executor.submit(() -> {
System.out.println("Task " + threadNumber + " running in: " + Thread.currentThread());
});
}
}
System.out.println("All tasks submitted!");
}
}
Output:
Task 4 running in: VirtualThread[#25]/runnable@ForkJoinPool-1-worker-4
Task 2 running in: VirtualThread[#23]/runnable@ForkJoinPool-1-worker-2
Task 1 running in: VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
Task 3 running in: VirtualThread[#24]/runnable@ForkJoinPool-1-worker-3
Task 5 running in: VirtualThread[#26]/runnable@ForkJoinPool-1-worker-5
All tasks submitted!
Each task runs in a Virtual Thread with an identifier (e.g., #25, #23).
* VirtualThread[#25]: The ID (#25) is assigned sequentially by the JVM to distinguish threads.
* These IDs (#21, #23, etc.) reflect the order in which Virtual Threads were created, not the order in which they started executing.
* The runnable state means the Virtual Thread is ready to execute or is currently executing the task. It’s an internal representation of the thread's lifecycle.
* The @ForkJoinPool-1-worker-4 part indicates that the Virtual Thread is currently running on a carrier thread in the ForkJoinPool.
Handling Blocking Operations with Virtual Threads
Virtual Threads make blocking operations, such as file I/O, efficient and straightforward without requiring asynchronous libraries or complex frameworks. Let’s explore a practical example: reading multiple files concurrently using Virtual Threads.
Example: File Processing with Virtual Threads
This example reads three files (file1.txt, file2.txt, and file3.txt) placed in the resources folder. Each file is processed in its own Virtual Thread managed by an executor.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
import java.util.stream.Collectors;
import java.util.concurrent.Executors;
public class FileProcessingWithExecutor {
public static void main(String[] args) {
// List of resource file names
List resourceFiles = List.of("file1.txt", "file2.txt", "file3.txt");
// Create an executor for Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
resourceFiles.forEach(resource -> executor.submit(() -> {
try (var inputStream = FileProcessingWithExecutor.class.getClassLoader().getResourceAsStream(resource)) {
if (inputStream == null) {
System.err.println("Error: File " + resource + " not found.");
return;
}
try (var reader = new BufferedReader(new InputStreamReader(inputStream))) {
String content = reader.lines().collect(Collectors.joining("\n"));
System.out.println("Read from " + resource + ": " + content);
}
} catch (Exception e) {
System.err.println("Error processing file " + resource + ": " + e.getMessage());
}
}));
} // Executor automatically shuts down when exiting try-with-resources
System.out.println("All file reading tasks submitted!");
}
}
Output:
Read from file1.txt: Hello from file1!
Read from file3.txt: Hello from file3!
Read from file2.txt: Hello from file2!
All file reading tasks submitted!
If a file is missing, you'll see:
Error: File file1.txt not found.
Error: File file2.txt not found.
Error: File file3.txt not found.
All file reading tasks submitted!
Example: Simulating Network Calls
Virtual Threads make handling blocking network calls simple and efficient:
import java.util.concurrent.Executors;
public class HttpSimulationWithVirtualThreads {
public static void main(String[] args) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 1; i <= 3; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " started on " + Thread.currentThread());
simulateHttpCall(taskId);
System.out.println("Task " + taskId + " finished on " + Thread.currentThread());
});
}
}
System.out.println("All tasks submitted!");
}
private static void simulateHttpCall(int taskId) {
try {
Thread.sleep(2000);
System.out.println("Task " + taskId + " got HTTP response!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Task " + taskId + " interrupted.");
}
}
}
Output:
Task 1 started on VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
Task 2 started on VirtualThread[#23]/runnable@ForkJoinPool-1-worker-2
Task 3 started on VirtualThread[#24]/runnable@ForkJoinPool-1-worker-3
Task 2 got HTTP response!
Task 3 got HTTP response!
Task 1 got HTTP response!
Task 2 finished on VirtualThread[#23]/runnable@ForkJoinPool-1-worker-1
Task 3 finished on VirtualThread[#24]/runnable@ForkJoinPool-1-worker-2
Task 1 finished on VirtualThread[#21]/runnable@ForkJoinPool-1-worker-4
All tasks submitted!
View the Source Code
All the examples in this blog are available on GitHub. Feel free to explore and try them out!