Skip to main content

6.6 Java 21 Virtual Threads & Structured Concurrency (Project Loom)

← Back to Chapter 6: Multithreading & Concurrency

Java 21 introduced Virtual Threads as an official LTS feature under Project Loom, fundamentally modernizing high-throughput server architecture.


1. Platform (OS) Threads vs Virtual Threads​

Traditional Platform Threads (1:1 with OS Kernel Threads):
JVM Thread 1 ──────► Linux Kernel pthread (1 MB fixed stack memory)
JVM Thread 2 ──────► Linux Kernel pthread (1 MB fixed stack memory)
* Limited to ~5,000–10,000 total threads before OS context-switch thrashing

Java 21 Virtual Threads (M:N User-Space Schedulers):
[ Millions of Virtual Threads ] (few hundred bytes on JVM Heap)
│ (Dynamic unmounting during blocking I/O)
▼
[ Carrier OS Threads ] (Equal to number of available CPU cores)
▼
[ CPU Hardware Cores ]
DimensionPlatform Threads (Thread)Virtual Threads (VirtualThread)
OS Mapping1:1 with OS kernel pthreadM:N (millions mapped to a few carrier threads)
Memory Footprint~1 MB fixed OS stack space~几百 bytes starting memory (dynamically sized on heap)
Creation CostExpensive kernel system callCheap Java heap object allocation
Blocking I/OOS thread stays blocked, wasting RAMThread unmounts from carrier thread, freeing carrier for others
Best Used ForCPU-bound calculationsI/O-bound microservices (HTTP, DB, Sockets)

2. Launching Virtual Threads​

// Method 1: Dedicated Executor (One virtual thread per task)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // Unmounts carrier thread!
return i;
});
});
} // Blocks until all 10,000 virtual tasks finish

// Method 2: Fluent Thread Builder
Thread vThread = Thread.ofVirtual().name("payment-worker-", 1).start(() -> {
System.out.println("Executing on carrier: " + Thread.currentThread());
});

3. The Thread Pinning Caveat​

A virtual thread cannot unmount from its carrier thread during blocking I/O if it is pinned:

  • Cause 1: Running inside a synchronized block or method.
  • Cause 2: Executing native code via JNI.
  • Solution: Replace synchronized blocks in I/O-intensive code with java.util.concurrent.locks.ReentrantLock.