Skip to main content

Virtual Threads (Project Loom)

Introduced as a standard feature in Java 21, Virtual Threads (developed under Project Loom) are lightweight threads that drastically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications.

Virtual threads aim to bring back the simplicity of the thread-per-request programming model without the scalability limitations of traditional operating system threads.


Platform Threads vs. Virtual Threads

Historically, every java.lang.Thread in Java was a Platform Thread, which maps directly 1:1 to an Operating System (OS) kernel thread.

FeaturePlatform Threads (OS Threads)Virtual Threads (JVM-managed)
ManagementManaged by the Operating System.Managed entirely by the Java Virtual Machine.
Creation CostExpensive. Takes ~1-2 milliseconds to start.Extremely cheap. Instantly instantiated.
Memory SizeReserves ~1MB of memory for the call stack.Starts at only a few hundred bytes, growing dynamically.
Concurrent LimitLimited by OS resources (usually a few thousand).Mapped to millions of concurrent active threads.
Blocking CodeBlocks the underlying OS thread, stalling execution.Unmounts automatically, keeping the OS thread active.

How Virtual Threads Work Under the Hood

Virtual threads run on top of platform threads, which act as Carrier Threads.

Mounting and Unmounting

  • Mounting: When a virtual thread is ready to run, the JVM schedules it on an available carrier thread.
  • Unmounting (Yielding): When a virtual thread encounters a blocking operation (such as Thread.sleep(), socket reading, or database queries), the JVM unmounts the virtual thread from the carrier thread and saves its stack state. The carrier thread is now free to execute another virtual thread. Once the blocking operation completes, the JVM schedules the virtual thread again.

Thread Pinning

In some situations, a virtual thread cannot be unmounted from its carrier thread. This is called pinning. Pinning occurs when:

  1. The virtual thread executes code inside a synchronized block or method.
  2. The virtual thread executes a native method (JNI call).

[!TIP] To avoid carrier thread pinning, prefer using ReentrantLock instead of synchronized blocks in high-concurrency parts of your code.


Creating Virtual Threads

Java provides multiple new APIs to construct virtual threads.

1. Using the Thread Builder

// Start a virtual thread immediately
Thread.ofVirtual().start(() -> {
System.out.println("Running on virtual thread: " + Thread.currentThread());
});

// Create an unstarted virtual thread
Thread vThread = Thread.ofVirtual().unstarted(() -> {
System.out.println("Task execution");
});
vThread.start();

Since virtual threads are extremely lightweight, you should never pool them. Instead, create an executor that spawns a new virtual thread for every single task:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> System.out.println("Task running"));
} // Executor automatically waits for all tasks to complete and closes

Practical Code Example: Running 100,000 Tasks

The following example demonstrates running 100,000 concurrent blocking tasks (each simulating a 1-second database latency).

Running this with platform threads would consume ~100GB of memory and crash the system. With Virtual Threads, it completes in a couple of seconds using minimal memory.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

public class VirtualThreadPerformance {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();

// Create an executor that creates a new virtual thread for each task
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {

// Submit 100,000 concurrent tasks
IntStream.range(0, 100_000).forEach(i -> {
executor.submit(() -> {
try {
// Simulate blocking I/O (e.g., database lookup)
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
});

} // Executor auto-closes and waits for all threads to finish

long endTime = System.currentTimeMillis();
System.out.println("Executed 100,000 blocking tasks!");
System.out.println("Total Time taken: " + (endTime - startTime) + " ms");
}
}