When to Use Virtual Threads

⭐ Interview Importance: MEDIUM
⏱️ Revision Time: 5 min

TL;DR

  • USE Virtual Threads for: I/O-bound workloads (Database queries, HTTP requests, File I/O).
  • DO NOT USE Virtual Threads for: CPU-bound workloads (Video encoding, heavy math, cryptography).
  • NEVER Pool Virtual Threads: Create a new one for every single task.

Concept

Virtual Threads are not magic pixie dust that makes Java execute code faster. They are a scaling mechanism.

The JVM scheduler multiplexes Virtual Threads onto a small number of physical CPU cores.
If a Virtual Thread encounters an I/O operation (e.g., HttpClient.send()), it says, “I’m going to be waiting for 200ms. Take me off the CPU so another thread can use it.” This is brilliant.

However, if a Virtual Thread encounters a CPU-bound operation (e.g., calculating the SHA-256 hash of a 1GB file), it never waits. It runs continuously. It never unmounts. If you have 4 CPU cores, and you launch 4 CPU-bound Virtual Threads, they will completely consume the underlying carrier threads. If you launch a 5th Virtual Thread, it will just sit in a queue forever, waiting for one of the first 4 to finish.

Examples

import java.util.concurrent.Executors;

public class VirtualThreadUsage {
    
    // --- SCENARIO 1: I/O BOUND (Perfect for Virtual Threads) ---
    public void fetchUrls() {
        // We create a new unpooled Virtual Thread per task.
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10000; i++) {
                executor.submit(() -> {
                    // Simulating a network call. Thread unmounts safely!
                    Thread.sleep(200); 
                    System.out.println("Fetched URL");
                    return null;
                });
            }
        }
    }
    
    // --- SCENARIO 2: CPU BOUND (Terrible for Virtual Threads) ---
    public void processVideoFrames() {
        // Use a standard Platform Thread Pool sized to your CPU cores!
        int cores = Runtime.getRuntime().availableProcessors();
        try (var executor = Executors.newFixedThreadPool(cores)) {
            for (int i = 0; i < 10000; i++) {
                executor.submit(() -> {
                    // Heavy math. Never yields. 
                    // Virtual threads would choke here. Platform threads are correct.
                    calculateCryptography(); 
                });
            }
        }
    }
}

Interview Questions

Q: Why shouldn’t you pool Virtual Threads?
A: Thread Pools were invented to solve a specific problem: OS Threads are expensive to create (taking milliseconds and megabytes). Therefore, we create them once, keep them alive in a pool, and reuse them.
Virtual Threads are cheap (taking microseconds and bytes). The overhead of managing a Virtual Thread Pool is actually higher than just creating a new Virtual Thread from scratch! Always create a fresh Virtual Thread for every task using Thread.ofVirtual().start().

Q: How do synchronized blocks affect Virtual Threads?
A: They cause “Thread Pinning”. If a Virtual Thread makes a blocking I/O call while holding a synchronized monitor lock, the JVM currently cannot unmount it. It gets permanently glued to the OS carrier thread until the I/O finishes, degrading performance. You should replace synchronized with ReentrantLock when writing code intended for Virtual Threads.