Sitemap
Syed Nazeer

Syed Nazeer

I'm a Java enthusiast, developing passion for technical content creation.

Virtual Threads

4 min readJun 2, 2026

--

1. What are Virtual Threads?

Virtual Threads = lightweight threads managed by JVM (not OS)

  • Introduced via Project Loom
  • Officially final in Java 21
  • Designed to handle millions of concurrent tasks efficiently

Unlike traditional threads:

  • They are not 1:1 mapped to OS threads
  • They are scheduled by JVM, not the OS

Key Characteristics

2. How Virtual Threads Work Internally

Platform Threads (Before)

1 Java Thread  →  1 OS Thread

Virtual Threads (Now)

Many Virtual Threads → Few OS Threads (Carrier Threads)

Concept: Mount / Unmount

  • When running → virtual thread is mounted on OS thread
  • When blocked (e.g., I/O) → it is unmounted
  • OS thread becomes free to handle another virtual thread

This is the game changer

3. Simple Example (Java 21)

public class VirtualThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
System.out.println("Running in: " + Thread.currentThread());
};
// Create virtual thread
Thread vt = Thread.ofVirtual().start(task);
vt.join();
}
}

Creating Many Threads

public class VirtualScale {
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
Thread.startVirtualThread(() -> {
// simulate work
});
}
}
}

This is practical with virtual threads
Impossible with platform threads

4. Executor-Based Usage (Real-world)

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10000)
.forEach(i -> executor.submit(() -> {
// blocking operations (DB, API calls)
Thread.sleep(100);
return i;
}));
}

Best use case: I/O-heavy applications

5. When to Use Virtual Threads

Perfect for:

  • REST APIs
  • Database calls
  • Microservices
  • Blocking I/O operations
  • High concurrency workloads

Not ideal for:

  • CPU-intensive tasks
  • Long-running compute-heavy jobs

6. Before Virtual Threads (Traditional Approach)

1. Thread Per Request Model (Old Way)

new Thread(() -> handleRequest()).start();

Problems:

  • High memory usage (~1MB per thread)
  • Limited scalability (~thousands max)
  • Context-switch overhead
  • Thread exhaustion

2. Thread Pools (Improved Old Approach)

ExecutorService pool = Executors.newFixedThreadPool(100);
pool.submit(() -> {
// work
});

Why Thread Pools?

Reuse threads
Avoid creation cost
Control concurrency

Limitations:

3. Asynchronous Programming (Reactive / CompletableFuture)

To avoid blocking:

CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(data -> process(data))
.thenAccept(result -> save(result));

Reactive Style (WebFlux, etc.)

Mono.fromCallable(() -> fetchData())
.map(this::process)
.subscribe();

Problems with Async Approach

7. Virtual Threads vs Old Approaches

Comparison Table

Press enter or click to view image in full size

8. Biggest Advantage

Virtual threads let you:

Get Syed Nazeer’s stories in your inbox

Join Medium for free to get updates from this writer.

Write simple blocking code
With non-blocking scalability

Old Style (Complex Async)

CompletableFuture.supplyAsync(() -> dbCall())
.thenApply(data -> apiCall(data))
.thenAccept(System.out::println);

Virtual Thread Style (Simple)

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
String data = dbCall(); // blocking
String result = apiCall(data); // blocking
System.out.println(result);
});
}

Same scalability, much cleaner code

9. Important Concepts

1. Carrier Threads

  • Backed by OS threads (ForkJoinPool)
  • Execute virtual threads

2. Pinning (Important for Interviews)

A virtual thread gets pinned when:

  • Inside synchronized
  • Native calls
  • Blocking operations not Loom-aware

Problem:

  • Prevents unmounting → hurts scalability

Example of Pinning

synchronized (this) {
Thread.sleep(1000); // blocks carrier thread
}

Use:

  • ReentrantLock instead of synchronized

3. Structured Concurrency (Java 21 Preview)

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> service1());
Future<String> f2 = scope.fork(() -> service2());
scope.join();
return f1.resultNow() + f2.resultNow();
}

Cleaner parallel code

10. Real-world Example (Spring Boot)

Works automatically if enabled:

spring.threads.virtual.enabled=true

Each request handled by virtual thread
No need for reactive frameworks

11. Migration Strategy

Replace:

  • Thread pools (I/O heavy code)
  • CompletableFuture chains (optional)
  • Reactive frameworks (in some cases)

Keep:

  • Async for CPU-heavy tasks
  • Parallel streams

12. Summary (Interview Style)

Virtual Threads:

  • Lightweight, JVM-managed threads
  • Enable millions of concurrent tasks
  • Best for I/O-bound workloads

Before:

  • Platform threads → expensive
  • Thread pools → limited scalability
  • Reactive → complex

Now: Simple blocking code + Massive scalability

Final One-Liner (Interview)

“Virtual threads in Java 21 decouple Java threads from OS threads, allowing millions of lightweight concurrent tasks while keeping a simple blocking programming model — eliminating the need for complex asynchronous code.”

--

--

Written by Syed Nazeer

I'm a Java enthusiast, developing passion for technical content creation.