Home Interview Java Virtual Threads: Modern Concurrency for Scalable Apps
Advanced 3 min · July 13, 2026

Java Virtual Threads: Modern Concurrency for Scalable Apps

Master Java Virtual Threads for high-throughput concurrency.

N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic understanding of Java concurrency (Thread, ExecutorService)
  • Familiarity with I/O-bound vs CPU-bound tasks
  • Java 21 or later installed
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Virtual threads are lightweight threads managed by the JVM, not the OS, enabling millions of concurrent tasks.
  • They are ideal for I/O-bound workloads, reducing overhead of platform threads.
  • Use Executors.newVirtualThreadPerTaskExecutor() to create virtual threads easily.
  • Virtual threads are not suitable for CPU-bound tasks; use platform threads or parallel streams.
  • They integrate with existing Java concurrency APIs like CompletableFuture and synchronized blocks.
✦ Definition~90s read
What is Java Virtual Threads and Modern Concurrency?

Virtual threads are lightweight, JVM-managed threads that enable high-throughput concurrency for I/O-bound tasks by efficiently mounting and unmounting on carrier threads.

Imagine a restaurant with a limited number of waiters (platform threads).
Plain-English First

Imagine a restaurant with a limited number of waiters (platform threads). Each waiter can only serve one customer at a time. Virtual threads are like having an infinite number of virtual waiters that can pause and resume instantly when waiting for food, allowing the restaurant to handle thousands of customers without hiring more waiters.

Concurrency in Java has evolved dramatically. For decades, developers relied on platform threads (OS threads) managed by the operating system. Each platform thread consumes significant memory (typically 1MB stack) and context-switching overhead, limiting the number of concurrent tasks to a few thousand. With the rise of microservices and high-throughput I/O-bound applications, this limitation became a bottleneck.

Enter Project Loom and Java Virtual Threads (introduced as a preview in Java 19 and finalized in Java 21). Virtual threads are lightweight threads managed by the Java Virtual Machine (JVM). They decouple the logical unit of concurrency (a task) from the physical OS thread. Millions of virtual threads can run on a handful of platform threads, dramatically improving scalability and resource utilization.

In this article, we'll dive deep into virtual threads: how they work under the hood, when to use them, and common pitfalls. You'll learn through code examples, production debugging scenarios, and real interview questions. By the end, you'll be equipped to answer any interview question on modern Java concurrency and apply virtual threads in your projects.

1. What Are Virtual Threads?

Virtual threads are lightweight threads that are managed by the JVM rather than the operating system. They are instances of java.lang.Thread, but they are not tied to a specific OS thread. Instead, the JVM schedules them on a small pool of platform threads (called carrier threads). When a virtual thread performs a blocking operation (like I/O), it is unmounted from the carrier thread, allowing another virtual thread to run. This mounting/unmounting is called 'continuation' and is extremely efficient.

Key characteristics
  • Creation is cheap: you can create millions of virtual threads without exhausting memory.
  • They are suitable for tasks that spend most of their time waiting (I/O-bound).
  • They integrate with existing Java APIs: Thread, ExecutorService, CompletableFuture, etc.
  • They do not improve CPU-bound tasks; for that, use platform threads or parallel streams.

Example: Creating a virtual thread

VirtualThreadExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class VirtualThreadExample {
    public static void main(String[] args) throws Exception {
        // Using Thread.Builder
        Thread vThread = Thread.ofVirtual().name("myVirtualThread").start(() -> {
            System.out.println("Hello from " + Thread.currentThread());
        });
        vThread.join();

        // Using Executors
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -> System.out.println("Hello from virtual thread via executor"));
        }
    }
}
Output
Hello from VirtualThread[#23,myVirtualThread]/runnable@ForkJoinPool-1-worker-1
Hello from virtual thread via executor
🔥Virtual Threads are Not Always Faster
📊 Production Insight
In production, always use Executors.newVirtualThreadPerTaskExecutor() for I/O tasks to avoid thread pool exhaustion.
🎯 Key Takeaway
Virtual threads are lightweight, JVM-managed threads ideal for I/O-bound concurrency.

2. Virtual Threads vs Platform Threads

Understanding the differences is crucial for interview questions. Here's a comparison:

  • Creation Cost: Platform threads are expensive (1MB stack, OS syscall). Virtual threads are cheap (few KB, no syscall).
  • Maximum Count: Platform threads limited to thousands. Virtual threads can be millions.
  • Context Switching: OS-level for platform threads; JVM-level for virtual threads (much faster).
  • Blocking Behavior: Platform threads block the OS thread; virtual threads are unmounted from carrier thread.
  • CPU-bound Tasks: Platform threads are better; virtual threads add scheduling overhead.
  • Compatibility: Virtual threads work with most existing Java code, but synchronized blocks and native methods can 'pin' the carrier thread.

Example: Comparing performance

ThreadComparison.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.concurrent.*;

public class ThreadComparison {
    public static void main(String[] args) throws Exception {
        int tasks = 10_000;

        // Platform threads
        long start = System.currentTimeMillis();
        try (var executor = Executors.newCachedThreadPool()) {
            CountDownLatch latch = new CountDownLatch(tasks);
            for (int i = 0; i < tasks; i++) {
                executor.submit(() -> {
                    try { Thread.sleep(10); } catch (InterruptedException e) {}
                    latch.countDown();
                });
            }
            latch.await();
        }
        long platformTime = System.currentTimeMillis() - start;
        System.out.println("Platform threads: " + platformTime + "ms");

        // Virtual threads
        start = System.currentTimeMillis();
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            CountDownLatch latch = new CountDownLatch(tasks);
            for (int i = 0; i < tasks; i++) {
                executor.submit(() -> {
                    try { Thread.sleep(10); } catch (InterruptedException e) {}
                    latch.countDown();
                });
            }
            latch.await();
        }
        long virtualTime = System.currentTimeMillis() - start;
        System.out.println("Virtual threads: " + virtualTime + "ms");
    }
}
Output
Platform threads: 1500ms
Virtual threads: 120ms
💡Interview Tip
📊 Production Insight
In production, mixing both types is common: use virtual threads for I/O, platform threads for CPU-intensive computations.
🎯 Key Takeaway
Virtual threads are cheaper and more scalable for I/O-bound tasks, but platform threads are better for CPU-bound tasks.

3. Virtual Threads Under the Hood: Continuations and Carrier Threads

Virtual threads are built on continuations. A continuation is a snapshot of the execution state (stack, program counter) that can be suspended and resumed. When a virtual thread blocks (e.g., on I/O), the JVM captures its continuation and unmounts it from the carrier thread. The carrier thread then picks up another virtual thread. This is called 'cooperative scheduling'.

The carrier threads are typically from a ForkJoinPool. When a virtual thread runs, it is mounted on a carrier thread. When it blocks, it is unmounted, and the carrier thread is free.

However, there are cases where a virtual thread cannot be unmounted, known as 'pinning'. This happens when: - The virtual thread executes a synchronized block or method. - The virtual thread calls a native method (JNI) or a foreign function.

When pinned, the carrier thread is blocked, reducing scalability. To detect pinning, use the JVM flag -Djdk.tracePinnedThreads=full.

Example: Pinning detection

PinningExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
public class PinningExample {
    public static void main(String[] args) throws Exception {
        // Run with -Djdk.tracePinnedThreads=full
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -> {
                synchronized (PinningExample.class) {  // This causes pinning
                    System.out.println("Inside synchronized block");
                    try { Thread.sleep(100); } catch (InterruptedException e) {}
                }
            }).get();
        }
    }
}
Output
Pinned carrier thread: VirtualThread[#23]/runnable@ForkJoinPool-1-worker-1
java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:...)
... (stack trace)
⚠ Pinning Can Kill Scalability
📊 Production Insight
In production, monitor for pinning using the trace flag. Refactor legacy code that uses synchronized heavily.
🎯 Key Takeaway
Virtual threads use continuations for lightweight scheduling; avoid pinning by minimizing synchronized blocks.

4. Using Virtual Threads with Existing APIs

Virtual threads integrate seamlessly with most Java concurrency APIs. You can use them with: - ExecutorService: Executors.newVirtualThreadPerTaskExecutor() creates an executor that starts a new virtual thread for each task. - CompletableFuture: Virtual threads can be used as the default executor for async operations. - ThreadLocal: Works with virtual threads, but be cautious: each virtual thread has its own ThreadLocal, which can increase memory usage. - Locks: ReentrantLock and other java.util.concurrent locks work well without pinning.

Example: CompletableFuture with virtual threads

CompletableFutureExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.concurrent.*;

public class CompletableFutureExample {
    public static void main(String[] args) throws Exception {
        var executor = Executors.newVirtualThreadPerTaskExecutor();
        CompletableFuture.supplyAsync(() -> {
            // Simulate I/O
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            return "Result";
        }, executor)
        .thenAcceptAsync(System.out::println, executor)
        .join();
        executor.shutdown();
    }
}
Output
Result
🔥Structured Concurrency
📊 Production Insight
When migrating legacy code, replace Executors.newCachedThreadPool() with Executors.newVirtualThreadPerTaskExecutor() for I/O tasks.
🎯 Key Takeaway
Virtual threads work with ExecutorService, CompletableFuture, and other APIs, making migration easy.

5. Common Pitfalls and Best Practices

  1. Pinning: As discussed, synchronized blocks and native methods pin carrier threads. Use ReentrantLock or synchronized only for short operations.
  2. ThreadLocal Abuse: Each virtual thread has its own ThreadLocal. Creating many virtual threads can lead to high memory usage. Use ScopedValue (incubator) as an alternative.
  3. CPU-bound Tasks: Do not use virtual threads for CPU-intensive work. They add overhead and can starve I/O tasks.
  4. Pool Sizing: With virtual threads, you don't need large thread pools. A small pool of carrier threads (e.g., number of CPU cores) is sufficient.
  5. Deadlocks: Virtual threads can still deadlock. Use proper locking order and timeouts.

Example: Avoiding pinning with ReentrantLock

AvoidPinning.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.concurrent.locks.ReentrantLock;

public class AvoidPinning {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws Exception {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -> {
                lock.lock();
                try {
                    System.out.println("Inside lock");
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    lock.unlock();
                }
            }).get();
        }
    }
}
Output
Inside lock
💡Best Practice
📊 Production Insight
In production, set -Djdk.tracePinnedThreads=full in staging to detect pinning before deploying.
🎯 Key Takeaway
Avoid synchronized blocks, be mindful of ThreadLocal, and use virtual threads only for I/O-bound tasks.

6. Interview Questions and Answers

Q1: What are virtual threads and how do they differ from platform threads? A: Virtual threads are lightweight threads managed by the JVM. They are cheap to create and can be unmounted from carrier threads when blocking. Platform threads are OS threads, expensive and limited.

Q2: When should you use virtual threads? A: For I/O-bound tasks like web servers, database calls, and network requests. Not for CPU-bound tasks.

Q3: What is pinning and how do you avoid it? A: Pinning occurs when a virtual thread cannot be unmounted, typically due to synchronized blocks or native methods. Avoid by using ReentrantLock or minimizing synchronized.

Q4: How do you create a virtual thread? A: Using Thread.ofVirtual().start() or Executors.newVirtualThreadPerTaskExecutor().

Q5: Can virtual threads improve performance of CPU-bound tasks? A: No, they add overhead. Use platform threads or parallel streams.

Example: Interview answer code snippet

InterviewAnswer.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Demonstrating virtual thread creation
public class InterviewAnswer {
    public static void main(String[] args) throws InterruptedException {
        // Method 1: Thread.Builder
        Thread vThread = Thread.ofVirtual().name("vt").start(() -> {
            System.out.println("Running in virtual thread");
        });
        vThread.join();

        // Method 2: ExecutorService
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -> System.out.println("Using executor"));
        }
    }
}
Output
Running in virtual thread
Using executor
🔥Interview Tip
🎯 Key Takeaway
Be ready to explain virtual threads, pinning, and appropriate use cases in interviews.
● Production incidentPOST-MORTEMseverity: high

The Thread Pool Meltdown: When Platform Threads Couldn't Keep Up

Symptom
Users experienced timeouts and HTTP 503 errors during peak hours.
Assumption
The developer assumed that increasing the thread pool size would handle the load.
Root cause
Each request spawned a blocking I/O operation (e.g., database call) that held a platform thread idle, exhausting the pool.
Fix
Replaced the fixed thread pool with virtual threads using Executors.newVirtualThreadPerTaskExecutor(), allowing thousands of concurrent I/O operations without thread starvation.
Key lesson
  • Platform threads are expensive; avoid blocking them on I/O.
  • Virtual threads are ideal for I/O-bound tasks.
  • Monitor thread pool utilization to detect bottlenecks.
  • Use structured concurrency to manage virtual thread lifecycles.
  • Test with realistic load to uncover thread exhaustion.
Production debug guideSymptom to Action3 entries
Symptom · 01
Application hangs or slow response under load
Fix
Check if virtual threads are being pinned due to synchronized blocks or native calls. Use -Djdk.tracePinnedThreads=full to log pinning events.
Symptom · 02
OutOfMemoryError: unable to create new native thread
Fix
Verify you are using virtual threads, not platform threads. Check thread dumps for excessive platform threads.
Symptom · 03
High CPU usage with many virtual threads
Fix
Virtual threads are not for CPU-bound tasks. Move CPU-intensive work to platform threads or parallel streams.
★ Quick Debug Cheat SheetCommon symptoms and immediate actions for virtual thread issues.
Thread pinning (synchronized block)
Immediate action
Replace synchronized with ReentrantLock or use java.util.concurrent locks.
Commands
-Djdk.tracePinnedThreads=full
jcmd <pid> Thread.dump_to_file -format=json dump.json
Fix now
Refactor to avoid blocking inside synchronized blocks.
Too many platform threads created+
Immediate action
Check if Executors.newCachedThreadPool() is used instead of virtual thread executor.
Commands
jstack <pid> | grep 'pool-'
jcmd <pid> VM.native_memory summary
Fix now
Switch to Executors.newVirtualThreadPerTaskExecutor().
Virtual thread stuck in I/O+
Immediate action
Check if the I/O operation is non-blocking (e.g., using java.nio.channels).
Commands
jcmd <pid> Thread.dump_to_file -format=json dump.json
grep 'BLOCKED' dump.json
Fix now
Use non-blocking I/O libraries or increase I/O timeout.
FeaturePlatform ThreadsVirtual Threads
Managed byOSJVM
Creation costHigh (1MB stack, syscall)Low (few KB, no syscall)
Maximum countThousandsMillions
Context switchingOS-level (expensive)JVM-level (cheap)
Blocking behaviorBlocks OS threadUnmounts from carrier
Suitable forCPU-bound tasksI/O-bound tasks
PinningNot applicablePossible with synchronized/native
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
VirtualThreadExample.javapublic class VirtualThreadExample {1. What Are Virtual Threads?
ThreadComparison.javapublic class ThreadComparison {2. Virtual Threads vs Platform Threads
PinningExample.javapublic class PinningExample {3. Virtual Threads Under the Hood
CompletableFutureExample.javapublic class CompletableFutureExample {4. Using Virtual Threads with Existing APIs
AvoidPinning.javapublic class AvoidPinning {5. Common Pitfalls and Best Practices
InterviewAnswer.javapublic class InterviewAnswer {6. Interview Questions and Answers

Key takeaways

1
Virtual threads are lightweight, JVM-managed threads ideal for I/O-bound concurrency.
2
Avoid pinning by minimizing synchronized blocks and native methods.
3
Use Executors.newVirtualThreadPerTaskExecutor() for easy migration.
4
Virtual threads are not for CPU-bound tasks; use platform threads instead.
5
Monitor pinning with -Djdk.tracePinnedThreads=full in staging.

Common mistakes to avoid

3 patterns
×

Using virtual threads for CPU-bound tasks

×

Heavy use of synchronized blocks in virtual threads

×

Creating too many virtual threads without control

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the concept of virtual threads and how they differ from platform...
Q02SENIOR
What is pinning in virtual threads and how can you avoid it?
Q03SENIOR
How would you design a high-throughput web server using virtual threads?
Q01 of 03JUNIOR

Explain the concept of virtual threads and how they differ from platform threads.

ANSWER
Virtual threads are lightweight threads managed by the JVM. They are cheap to create and can be unmounted from carrier threads when blocking, allowing millions of concurrent tasks. Platform threads are OS threads, expensive and limited. Virtual threads are ideal for I/O-bound tasks, while platform threads are better for CPU-bound tasks.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Are virtual threads available in Java 17?
02
Can virtual threads improve performance of CPU-bound tasks?
03
How do I detect if a virtual thread is pinned?
04
Can I use ThreadLocal with virtual threads?
05
What is the default carrier thread pool size?
N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
🔥

That's Java Interview. Mark it forged?

3 min read · try the examples if you haven't

Previous
Java Microservices Interview Questions
8 / 8 · Java Interview
Next
Top 50 Python Interview Questions