Java 21+ Virtual Threads: Use Cases, Scheduling Boundaries, and Migration Strategies
Baseline: Java 21 LTS. Java 24/25 virtual thread implementation and structured concurrency APIs need to be rechecked against the target JDK.
1. What It's Good For
Virtual threads are well-suited for tasks that spend a lot of time blocked on network, file, or database I/O. When blocked, virtual threads can be unmounted, allowing platform threads to continue handling other work.
They won't give CPU-bound tasks more compute power, nor will they increase the capacity of database connection pools, remote services, or thread-safe collections.
2. Minimal Usage
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<Response> future = executor.submit(() -> client.fetch(request));
Response response = future.get();
}
Don't submit virtual threads to an unbounded downstream call chain. Use semaphores, connection pools, and request budgets to limit concurrency.
3. Migration Checklist
Check whether ThreadLocal holds large objects, whether blocking calls are truly unmountable, whether there are synchronized critical sections, lock contention, JNI or native blocking, and whether cancellation and timeouts can propagate.
Pinning diagnostics from the JDK 21 era are still worth keeping; reread the corresponding JEPs and release notes after upgrading the target JDK—don't treat any version's internal implementation as an API contract.
4. Compared to Async APIs
Virtual threads let synchronous blocking code run with lower thread cost, while async APIs avoid blocking through explicit continuations or event loops. Choose based on your team's model, library compatibility, debugging approach, and downstream capacity—not just thread counts.
5. Load Testing Approach
Test platform threads, virtual threads, and async implementations separately for throughput, latency, memory, cancellation, and failure recovery using the same connection pool, timeouts, load, and downstream service. Don't use CPU-intensive benchmarks to draw I/O conclusions.