Zademy

Virtual Threads in Java: Scalability Without Complexity

Java; VirtualThreads; ProjectLoom; Concurrency; JDK25
509 words

The Problem of Traditional Concurrency

Java applications have historically relied on platform threads (Platform Threads or traditional threads) for concurrency.

Platform Threads (PT):

  • Wrappers around OS-managed threads with 1
    mapping.
  • Expensive and heavyweight, each consuming ~1-2 MB of memory and OS resources.
  • Limited by system resources. Creating too many leads to OutOfMemoryError or saturates context switching.
  • In web apps, the "one thread per request" (Thread-per-Request) model is intuitive but doesn't scale well with high concurrency (e.g., >10k concurrent requests).

The Need: In high-concurrency scenarios like microservices, streaming, or trading APIs, low latency and high scalability are required. Solutions like reactive programming (Project Reactor, RxJava) improve throughput but complicate code and debugging.

What Are Virtual Threads (VT)?

Introduced by Project Loom and stabilized in JDK 21 (JEP 444), Virtual Threads are Java's solution for massive concurrency without asynchronous programming.

Key Features:

  • Lightweight Threads: Instances of java.lang.Thread managed by the JVM (~1 KB per thread).
  • Millions of Threads: Allows creating millions of threads without exhausting memory.
  • I/O-bound Scalability: Optimized for throughput in blocking tasks (not speed-up in CPU-bound).
  • Full Compatibility: Drop-in replacement for existing Thread code. Similar to Go's goroutines.
  • Available in JDK 21+: Stable since JDK 21; ongoing improvements in JDK 22-25.

Internal Working: M
Scheduling

Mapping M:N: M virtual threads (millions) over N platform threads (CPU cores).

  1. Carriers: Platform thread that "carries" a VT.
  2. Mount/Unmount: VT mounts on carrier to execute; unmounts on I/O blocks (sleep(), sockets, DB queries).
  3. JVM Scheduler: Dynamically reassigns carriers. Carrier stays free during I/O waits.
  4. Continuation: On I/O completion, VT returns to scheduler and remounts on any available carrier.

This maximizes CPU utilization: carriers always busy.

Recent Updates:

  • JDK 24 (JEP 491): Eliminated pinning in synchronized blocks (VTs unmount correctly).
  • JDK 25 (JEP 506): Scoped Values as immutable alternative to ThreadLocal, preventing memory leaks in VTs.

Practical Examples

Optimized for I/O-bound (HTTP, DB, files).

Example 1: Simple Virtual Thread

Thread.startVirtualThread(() -> {
    System.out.println("Hello from VT! " + Thread.currentThread());
});

Example 2: ExecutorService (High Concurrency)

import java.util.concurrent.Executors;
import java.time.Duration;

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // 10k concurrent tasks
    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1)); // Simulates I/O
            // Business logic
        })
    );
} // Auto-awaits completion

Example 3: Spring Boot + VT (Real Benchmark)

In recent benchmarks (JDK 24 + Spring Boot + Postgres), VTs reduce latency ~50% vs platform threads at 10k req/s.

@RestController
public class ApiController {
    @GetMapping("/data")
    String getData() {
        // VT handles request automatically in Tomcat/Undertow with config
        return fetchFromDb(); // Blocks but scales
    }
}

Advanced Considerations and Best Practices

CPU-bound vs I/O-bound

TypeRecommendationReason
I/O-bound (DB, HTTP)VT + newVirtualThreadPerTaskExecutor()Massive scalability
CPU-bound (crypto, ML)Platform Threads pool = CPU coresAvoids carrier starvation

Issues and Solutions

  • Pinning: Resolved in JDK 24 for synchronized. Avoid JNI/natives.
  • ThreadLocal: ❌ With VT (memory explosion). Use Scoped Values (JDK 25 preview).
  • Metrics/Monitoring: Thread.currentThread().isVirtual(); JFR supports VTs since JDK 21.
  • Frameworks: Spring Boot 3.2+, Quarkus, Helidon native.

Best Practices (2025):

  1. Use VTs by default in web servers.
  2. Profile with JDK Mission Control/JFR to detect pinning.
  3. Migrate ThreadLocal to Scoped Values.
  4. Fixed pools for CPU-intensive.
  5. Test with wrk/ab for >100k concurrent.

Final Summary

ProfileUse VT?
BeginnerYes, Thread.startVirtualThread() for all concurrent.
ExpertYes for I/O; No for pure CPU. Monitor pinning.

Metaphor: PT = Dedicated elevators per person (expensive). VT = Pneumatic tube system: thousands of lightweight capsules in few physical tubes.

Sources: OpenJDK JEPs, Spring Boot benchmarks (Reddit/Java 24), RockTheJVM/RabinaNPatra guides (2025).