Unlock Massive Concurrency with Java Virtual Threads: A Deep Dive
This article explores Java virtual threads introduced in JDK 21, detailing their architecture, implementation, practical usage with Thread and Executors APIs, performance comparisons against platform threads, best‑practice guidelines, and benchmark results demonstrating superior throughput for high‑concurrency server applications.
1. Background
Virtual threads are implemented by the JDK runtime rather than the operating system, allowing millions of lightweight threads to run concurrently within a single Java process. They enable higher throughput for thread‑per‑request servers by mapping many virtual threads onto a limited number of platform threads.
2. Implementation
Virtual threads are instances of java.lang.Thread that are not bound to a specific OS thread. When a virtual thread performs a blocking I/O operation, the runtime suspends it and frees the carrier platform thread for other work.
The core components are:
Continuation : wraps the user task; when the task blocks, the continuation yields.
Scheduler : a ForkJoinPool (DEFAULT_SCHEDULER) that dispatches virtual threads onto platform threads.
Carrier : the platform thread that actually executes the virtual thread’s code.
runContinuation : a Runnable that loads the virtual thread onto a carrier before execution and unloads it afterward.
3. Usage
Creating a virtual thread directly:
Thread.Builder.OfVirtual builder = Thread.ofVirtual().name("worker-", 0);
Thread worker = builder.start(this::doSomething);
worker.join();Using an executor that creates a new virtual thread per task:
try (ExecutorService es = Executors.newVirtualThreadPerTaskExecutor()) {
Future<?> f = es.submit(this::doSomething);
f.get();
}Server example that accepts client connections and handles each with a virtual thread:
public class Server {
public static void main(String[] args) throws IOException {
Set<String> platforms = new HashSet<>();
try (ServerSocket ss = new ServerSocket(9999)) {
Thread.Builder.OfVirtual clientBuilder = Thread.ofVirtual().name("client", 1);
while (true) {
Socket s = ss.accept();
clientBuilder.start(() -> {
String name = Thread.currentThread().toString().split("@")[1];
platforms.add(name);
// handle I/O ...
});
}
}
}
}Client that spawns many virtual threads to connect to the server:
public class Client {
public static void main(String[] args) throws InterruptedException {
Thread.Builder.OfVirtual b = Thread.ofVirtual().name("client", 1);
for (int i = 0; i < 100_000; i++) {
b.start(() -> {
try (Socket s = new Socket("localhost", 9999)) {
// send/receive ...
} catch (IOException e) { /* handle */ }
});
}
Thread.sleep(Long.MAX_VALUE);
}
}4. Scheduling and Pinning
When a virtual thread runs, the Java runtime schedules it onto a carrier platform thread. If the virtual thread blocks, it is unmounted, allowing the carrier to run other virtual threads. Pinning occurs when a virtual thread executes inside a synchronized block or calls native code; the carrier is not released, which can reduce scalability. Use ReentrantLock instead of long‑running synchronized blocks to avoid frequent pinning.
5. Best Practices
Write simple synchronous code and use blocking I/O; virtual threads make this cheap.
Do not pool virtual threads—create a new one per task.
Use Semaphore to limit concurrency when external services cannot handle unlimited parallelism.
Avoid caching expensive objects in ThreadLocal for virtual threads, as they are not reused.
Replace long‑running synchronized sections with ReentrantLock to prevent prolonged pinning.
6. Performance Tests
Environment: OpenJDK 21.0.4 on a Windows 11 machine with an i5‑14600KF (14 cores, 20 threads). The benchmark compares a virtual‑thread executor with fixed‑size platform‑thread pools.
public class PerformanceTest {
private static final int REQUEST_NUM = 10_000;
public static void main(String[] args) {
// run tests for virtual threads and platform threads of various sizes
}
// testVirtualThread, testPlatformThread, handleRequest implementations ...
}Results (average over three runs):
Virtual thread avg time: 437 ms
Platform thread[200] avg time: 15 549 ms
Platform thread[500] avg time: 6 232 ms
Platform thread[800] avg time: 4 056 ms
Platform thread[1000] avg time: 3 138 msThe virtual‑thread implementation processes all 10 000 requests in under half a second, while platform‑thread pools suffer from thread‑pool limits and higher latency.
A simple Spring Boot WebFlux service using virtual threads achieved comparable throughput to a reactive Netty implementation, but with far simpler code.
7. Conclusion
Java virtual threads simplify high‑concurrency programming, provide near‑unlimited scalability, and dramatically improve server throughput without requiring complex reactive frameworks. As the technology matures, virtual threads are poised to become a standard practice for building scalable Java back‑end services.
References
Virtual Threads – Oracle Documentation
JEP 444: Virtual Threads – OpenJDK
Spring WebFlux – SpringDoc
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Alibaba Cloud Developer
Alibaba's official tech channel, featuring all of its technology innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
