CodeGym /Courses /JAVA 25 SELF /Scalability and Performance of Virtual Threads

Scalability and Performance of Virtual Threads

JAVA 25 SELF
Level 57, Lesson 2
Available

1. Scalability

Why do platform threads scale poorly?

Each classic thread (Thread) is an operating-system entity with its own stack (usually 1–2 MB) and state structures. Attempting to create, say, 10 000 platform threads often results in an OutOfMemoryError. That’s why traditional servers use bounded thread pools.

Virtual threads: the magic of scalability

Virtual threads (Java 21+) are “lightweight” threads managed by the JVM. Their stack lives on the heap and can grow/shrink dynamically. When a thread blocks on I/O, the JVM “parks” it and continues running other tasks.

Inside the JVM, a small pool of “carriers” (carrier threads) — OS platform threads — takes turns executing virtual threads. This allows creating 100 000+ tasks without memory blowups. The JVM itself schedules which virtual threads run when.

Demo: 100_000 virtual threads vs. 1_000 platform threads

Example: creating 1,000 platform threads

// Attempt to create 1,000 platform threads
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    Thread t = new Thread(() -> {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
    threads.add(t);
    t.start();
}
System.out.println("Threads created: " + threads.size());

Result: On most systems you’ll manage to create 1 000–2 000 threads; at higher values you’ll hit memory issues and slowdowns.

Example: creating 100 000 virtual threads

// Create 100,000 virtual threads
List<Thread> vThreads = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
    Thread t = Thread.ofVirtual().start(() -> {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
    vThreads.add(t);
}
System.out.println("Virtual threads created: " + vThreads.size());

Result: The program easily creates 100 000 virtual threads without crashes or significant slowdowns. Memory usage is dramatically lower.

Visual comparison

Thread type Max threads (approx.) Memory usage Startup time
Platform (Thread) 1 000 – 10 000 High Slow
Virtual 100 000 – 1 000 000+ Low Instant

Fact: Virtual threads let you write “one thread per task” code without complex pools and without risking system overload.

2. Performance: where virtual threads shine

I/O-bound workloads

Virtual threads are great for network calls, file I/O, and databases. When an operation blocks, a virtual thread releases its carrier, and the JVM runs other tasks. This increases throughput with many concurrent waits.

Example: simulating 10 000 concurrent HTTP requests

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

HttpClient client = HttpClient.newHttpClient();
List<Thread> threads = new ArrayList<>();

for (int i = 0; i < 10_000; i++) {
    Thread t = Thread.ofVirtual().start(() -> {
        try {
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://example.com"))
                .build();
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Response: " + response.statusCode());
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
        }
    });
    threads.add(t);
}

// Wait for all threads to finish
for (Thread t : threads) {
    t.join();
}

Result: All 10 000 requests execute concurrently; the program doesn’t crash, and the code remains simple.

CPU-bound: virtual threads don’t speed up computation

If a task is CPU-bound, virtual threads won’t make it faster: the number of cores is fixed. Here, fixed-size pools equal to the number of cores are appropriate to avoid excessive contention.

// Each task computes the sum of a large range
Runnable cpuTask = () -> {
    long sum = 0;
    for (int i = 0; i < 100_000_000; i++) {
        sum += i;
    }
    System.out.println("Sum: " + sum);
};

// Start 1,000 virtual threads with computations
for (int i = 0; i < 1000; i++) {
    Thread.ofVirtual().start(cpuTask);
}

Result: Threads will compete for the CPU, but there will be no speedup—this isn’t a job for Virtual Threads.

3. Limitations and caveats

Synchronization and virtual-thread pitfalls

  • Beware of native locks. Using synchronized can pin a virtual thread to its carrier, reducing the benefit. Prefer ReentrantLock, Semaphore, and other primitives from java.util.concurrent optimized for virtual threads.
  • Legacy libraries. Some JDBC drivers and native libraries aren’t yet optimized for Virtual Threads. Carefully test blocking operations.

Not for long-lived tasks

Virtual Threads are ideal for “short” units of work: handle a request, perform one operation, and finish. A million never-ending tasks (e.g., endless computations) won’t help—use platform threads for those.

4. Best practices: where to use virtual threads

  • I/O-bound tasks: network calls, files, databases—anywhere a thread spends a lot of time waiting.
  • Web servers: handle each HTTP request in a separate virtual thread.
  • Integration tests: quickly simulate thousands of clients.
  • Asynchronous processing: write familiar “blocking” code—the JVM does smart scheduling.

Avoid using them for:

  • Tasks that constantly saturate the CPU.
  • Situations where compatibility with low-level libraries is critical (not all are adapted yet).

Under the hood, it’s convenient to use the executor: Executors.newVirtualThreadPerTaskExecutor()—“one virtual thread per task,” with no fixed pool.

5. Monitoring and measurement: how to see virtual threads in action

JVisualVM and Flight Recorder

JVisualVM shows active threads, their states, and memory; starting with Java 21, virtual threads are displayed separately. Java Flight Recorder (JFR) records a detailed “black box” of execution, including Virtual Threads statistics—great for finding bottlenecks.

How to check the number of threads in code

A simple way to see the number of threads in the JVM:

System.out.println("Total threads: " + Thread.activeCount());

Count how many of them are virtual:

long vCount = Thread.getAllStackTraces().keySet().stream()
    .filter(Thread::isVirtual)
    .count();
System.out.println("Virtual threads: " + vCount);

6. Common mistakes when working with virtual threads

Error #1: Using virtual threads for heavy computations. Spawning millions of virtual threads with CPU-bound tasks won’t speed up the processor. Virtual Threads are not a “turbo” for computation.

Error #2: Blindly copying old patterns. Don’t create fixed pools of virtual threads. Use Executors.newVirtualThreadPerTaskExecutor() and let the JVM scale automatically.

Error #3: Using unsupported libraries. Native locks and libraries not adapted to Loom can cause stalls and performance drops. Check compatibility in advance.

Error #4: Premature optimization. If you have only a few threads and conventional multithreading, don’t rush to move everything to Virtual Threads. The tool shines where there’s massive I/O and waiting.

Error #5: Ignoring monitoring. Creating a million tasks is easy, but without monitoring and exception handling you may end up with a “pretty benchmark” instead of a reliable system. Use JVisualVM and JFR.

1
Task
JAVA 25 SELF, level 57, lesson 2
Locked
Virtual City Population Monitoring 🏙️
Virtual City Population Monitoring 🏙️
1
Task
JAVA 25 SELF, level 57, lesson 2
Locked
Cloud compute server load simulation ☁️
Cloud compute server load simulation ☁️
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION