Java Virtual Threads in Practice — Practical Guide (Apr 20, 2026)
Java Virtual Threads in Practice
Level: Intermediate
Date: April 20, 2026
Introduction
Java Virtual Threads, introduced as a stable feature in JDK 21, have revolutionised concurrent programming on the JVM. They offer a lightweight, highly scalable alternative to traditional platform threads, enabling thousands or even millions of concurrent threads with minimal overhead. This article guides you through practical usage of virtual threads, clarifies when to choose them over platform threads, and highlights common pitfalls.
Prerequisites
- JDK Version: Use
JDK 21or later, as virtual threads became stable starting in JDK 21. Earlier versions (e.g., JDK 19 and 20) included virtual threads as preview features—avoid using them in production before stability. - Java Basics: Strong knowledge of Java concurrency primitives (threads, executors).
- IDEs & Tools: Updated IDE (IntelliJ IDEA 2023.2+, Eclipse 2023-06+), or command-line with
javacandjavafrom JDK 21. - Build System: Maven/Gradle configured for JDK 21+ to compile and run code with virtual threads.
Hands-on Steps
Creating and Running Virtual Threads
Virtual threads are created using Thread.startVirtualThread(Runnable), or through the Executors.newVirtualThreadPerTaskExecutor() factory. Unlike platform threads, virtual threads are managed by the JVM scheduler, not the operating system, making their creation and context switching inexpensive.
// Starting a simple virtual thread directly
Thread vThread = Thread.startVirtualThread(() -> {
System.out.println("Hello from a virtual thread!");
});
vThread.join();
This runs a task without needing a traditional thread pool.
Using Virtual Threads with Executors
For repeated or parallel tasks, leverage an ExecutorService that uses virtual threads:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
System.out.println("Running in virtual thread pool: " + Thread.currentThread());
});
// Additional tasks...
} // Executor auto-closes, terminating virtual threads gracefully.
When to Choose Virtual Threads vs Platform Threads
- Virtual threads: Suit IO-bound and high-concurrency workloads such as web servers, database requests, or reactive systems that traditionally rely on asynchronous programming.
- Platform threads: Preferred for CPU-bound tasks, or when precise control over thread priorities and OS-level integration is required (e.g., real-time apps).
Virtual threads can dramatically simplify code by replacing callbacks or reactive streams with direct sequential logic.
Common Pitfalls
Blocking Calls in Virtual Threads
Virtual threads handle blocking calls efficiently because their lightweight design means blocking does not prevent scheduling other virtual threads. However, beware when mixing virtual threads with code that depends on thread-local state or native thread ID, as these can behave differently.
Thread-Local Variables
Because virtual threads may be reused across different OS threads, the traditional ThreadLocal API can lead to unexpected behaviour. Keep thread-local usage minimal or prefer scoped context propagation mechanisms like java.lang.ThreadLocal-compatible libraries or JEP 429: Scoped Values.
Resource Limits and Monitoring
Despite low overhead, virtual threads are not free. Reasonable limits around millions of concurrent threads will depend on system resources and workload patterns. Use OS monitoring and Java Flight Recorder to observe thread lifecycle and resource footprints.
Validation
Confirming Virtual Thread Creation
Use simple diagnostics to confirm your thread is virtual:
System.out.println("Thread: " + Thread.currentThread() +
", Virtual: " + Thread.currentThread().isVirtual());
Profiling & Monitoring
Employ Java Flight Recorder (JFR) with the built-in “Virtual Thread” events to view thread creation, scheduling, and blocking times. Tools like VisualVM or JMC (Java Mission Control) support visualising virtual thread activity.
Checklist / TL;DR
- Use
JDK 21+for stable virtual threads. - Create virtual threads via
Thread.startVirtualThread()orExecutors.newVirtualThreadPerTaskExecutor(). - Prefer virtual threads for IO-heavy, high-concurrency workloads.
- Minimise use of
ThreadLocal, prefer scoped context APIs. - Monitor virtual thread count and resource use with JFR.
- Testing for virtual thread is via
Thread.isVirtual(). - Platform threads remain suitable for CPU-bound and native-thread dependent workloads.