Virtual Threads
NOTE: This is a preview feature NOT enabled by-default in JDK-21.
Virtual Threads are introduced under Project Loom and are part of JPE-436.
We all might have heard about Threads in Java. They are old, very old, since JDK 2 released in 1998. 🕰
They did a very good job in concurrent processing. But we must admit, in this ever changing world of programming, they have few problems:
- Even though they were light weight processes, but the Threads were just a thin wrapper around the operating system threads (the OS Threads). It was costly to create them and having a number of Threads running concurrently was practically limited by the system’s hardware, since they are mapped 1:1 to the OS threads.
- With the advancements in the J2EE, every incoming request is handled by a thread (thread-per-request pattern). In the today’s world of microservices, a single request can fetch or update data on multiple systems. When the application waits for the response from other microservices, the current thread remains in the idle state.
- Also, with the advent of Reactive Programming, much of the request-response have gone asynchronous. Information is request by a separate thread and the response is accepted on a separate thread, it makes it very difficult to debug. 😮💨
Virtual Threads
Virtual Threads are also instances of java.lang.Thread. They run their code on the underlying OS thread, but DON’T block the OS thread for its entire execution. Multiple virtual threads can be associated to a single OS thread.
We can create millions ($ 10^ 6 $) of virtual threads independent of our system hardware. Virtual Threads are managed by JVM, so the overhead of context-switching is avoided.
Virtual Threads do not block OS threads while they are not running. They are best suited for asynchronous APIs for achieving high scalability and throughput.
Differences between Classic Threads and Virtual Threads
- Virtual Threads are daemon threads: Means JVM won’t wait for them to be completed when exiting.
- Virtual Threads always have normal priority.
- Virtual Threads are not active members of thread group.: Call to Thread.getThreadGroup() on a virtual thread gives a generic name “VirtualThreads”.
- Virtual Threads do not support stop(), resume() or suspend() methods.
Execution and Performance Comparison of Classic Threads and Virtual Threads
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
public class VirtualThreads {
public static void main(String args[]) {
final AtomicInteger increment = new AtomicInteger();
Runnable runnable = () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(e);
}
};
// Classic Threads
long startTime = System.currentTimeMillis();
try (var executor = Executors.newFixedThreadPool(100)) {
IntStream.range(0, 10_000).forEach(i -> executor.submit(runnable));
}
long endTime = System.currentTimeMillis();
System.out.println(String.format("Classic Threads Total Time Taken: %d milliseconds", endTime - startTime));
// Virtual Threads
startTime = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> executor.submit(runnable));
}
endTime = System.currentTimeMillis();
System.out.println(String.format("Virtual Threads Total Time Taken: %d milliseconds", endTime - startTime));
}
}
Since its a preview feature, similar to String Templates, they are NOT enabled by default. Run the above code as:
java --enable-preview --source 21 VirtualThreads.java
Output is:
Classic Threads Total Time Taken: 100798 milliseconds
Virtual Threads Total Time Taken: 1823 milliseconds
This huge difference in the execution time is due to the fact that the Executor service have created around 10K Virtual Threads since they are very light weight and doesn’t require overhead of context switching as Classic threads do.
I tried experimenting with the fixed pool size of Classic threads. Even when I increased its size to 10K via Executors.newFixedThreadPool(10000)
, I still got better performance for Virtual Threads (~3x more faster).
Classic Threads Total Time Taken: 3569 milliseconds
Virtual Threads Total Time Taken: 1366 milliseconds