Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,068visitors

Built for engineers who want to go deep.


← Java 21 — New Features

Data-Oriented Programming

  • Records
  • Sealed Classes
  • Pattern Matching

Virtual Threads (Project Loom)

  • Virtual Threads
Chaturmind
← Java 21 — New Features

Data-Oriented Programming

  • Records
  • Sealed Classes
  • Pattern Matching

Virtual Threads (Project Loom)

  • Virtual Threads
HomeLearnJavaJava 21 — New FeaturesVirtual Threads (Project Loom)
✓ FreeAdvanced· 6 min read

Virtual Threads

Millions of cheap threads — how Project Loom changes Java server-side concurrency.

Published September 21, 2026


Virtual Threads

Server applications spend most of their time waiting: for a database, for another service's HTTP response, for a file. In the classic thread-per-request model, each waiting request holds an OS (platform) thread, and OS threads are expensive: roughly a megabyte of stack each, with creation and scheduling handled by the kernel. That caps a typical server at a few thousand concurrent requests. To go further, developers moved to asynchronous, reactive code, which scales but is much harder to write, debug and profile.

Virtual threads (final since Java 21, from Project Loom) remove that trade-off. They're threads managed by the JVM, not the OS, and they're cheap enough to create millions. You can write simple, blocking, one-request-per-thread code, and it scales like asynchronous code.

Advertisement

How they work

A virtual thread is a Java object holding its stack in heap memory. The JVM runs virtual threads on a small pool of platform threads called carrier threads (a ForkJoinPool, by default one per core).

 virtual threads (millions):   V1  V2  V3  V4  V5  V6 …
                                │ mounted when runnable
 carrier threads (≈ cores):    C1  C2  C3  C4

When a virtual thread performs a blocking operation (socket I/O, Thread.sleep, BlockingQueue.take, Lock waits and most JDK blocking calls), the JVM unmounts it: it saves its stack to the heap and frees the carrier to run another virtual thread. When the operation completes, the virtual thread is scheduled onto a carrier again. The blocking call looks synchronous to your code, but no OS thread sits idle waiting.

Creating virtual threads

Thread.startVirtualThread(() -> handle(request));

Thread t = Thread.ofVirtual().name("worker-", 0).start(task);

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<Price>> prices = suppliers.stream()
            .map(s -> executor.submit(() -> s.fetchPrice(item)))   // one virtual thread per task
            .toList();
    // ... use the futures
}   // close() waits for all tasks to finish

Don't pool virtual threads. Pooling exists because platform threads are expensive. Virtual threads are cheap, so create one per task, and let them finish.

In Spring Boot 3.2+, one property runs request handling (Tomcat or Jetty), @Async tasks and more on virtual threads:

spring.threads.virtual.enabled=true

Where they help, and where they don't

They help with high-concurrency, I/O-bound workloads: web services calling databases and other services, and fan-out calls to many downstream APIs. Throughput can rise dramatically, because concurrency is no longer limited by the thread count.

They don't help with:

  • CPU-bound work. There are still only as many cores as before. Use a platform thread pool sized to the cores, or parallel streams.
  • Latency of a single request. A virtual thread doesn't make one operation faster.
  • Bottlenecks elsewhere. If the database allows 20 connections, 10,000 virtual threads will simply queue for them.

Pitfalls

1. Limit concurrency explicitly

Thread pools used to limit concurrency as a side effect. With virtual threads, nothing stops 50,000 requests from hitting a downstream service at once. Protect downstream services deliberately:

private final Semaphore paymentPermits = new Semaphore(50);

Receipt charge(Order order) throws InterruptedException {
    paymentPermits.acquire();
    try { return paymentClient.charge(order); }
    finally { paymentPermits.release(); }
}

Connection pools (HikariCP) still bound database concurrency. Size them for what the database can handle, not for the number of threads.

2. Pinning

Some situations pin a virtual thread to its carrier, so blocking also blocks the carrier:

  • blocking inside a synchronized block or method (in Java 21–23);
  • blocking inside native code or foreign functions.

If many virtual threads block while pinned, the few carriers are exhausted and throughput collapses. For Java 21–23, replace synchronized around blocking I/O with ReentrantLock. Java 24 (JEP 491) removed pinning for synchronized, so on current JDKs this is much less of a concern, though native calls still pin. You can diagnose pinning with the JFR event jdk.VirtualThreadPinned.

3. ThreadLocal at scale

ThreadLocal works, but with a million threads, a large per-thread cached object (a buffer, a formatter) multiplies into a million copies. Avoid using ThreadLocals as caches of expensive objects. For passing context such as request IDs, scoped values (ScopedValue, finalized in recent JDKs) are the lighter-weight replacement.

Structured concurrency

A related Loom feature (in preview through several releases) treats a group of concurrent subtasks as one unit: if one fails, the others are cancelled, and the parent waits for all of them before continuing. It prevents leaked threads and makes fan-out code read like sequential code. Where it's available with preview features enabled, StructuredTaskScope is the API. Otherwise, newVirtualThreadPerTaskExecutor() in try-with-resources gives you the "wait for all" part.

Virtual threads vs reactive programming

Reactive frameworks (WebFlux, Reactor) also achieve high concurrency, but with a different programming model: operator chains, no blocking calls allowed, stack traces that don't show your call path, and harder debugging. Virtual threads give similar scalability for most I/O-bound services with ordinary imperative code, standard debugging and profiling, and existing blocking libraries (JDBC, the classic HTTP clients). Reactive still offers streaming and backpressure operators, which remain useful for stream-processing problems.

Follow-up questions this topic invites — and their answers

Q: Are virtual threads faster than platform threads? A: Not per operation. The benefit is scale: you can have vastly more concurrent blocking tasks, so throughput rises for I/O-bound workloads. CPU-bound code gains nothing.

Q: Should I pool virtual threads? A: No. Create one per task. Pooling defeats their purpose. To limit concurrency against a resource, use a semaphore or a bounded connection pool instead.

Q: What is pinning, and how do you avoid it? A: Pinning is when a virtual thread can't unmount from its carrier while blocked, so it blocks the carrier too. In Java 21–23 it happens when blocking inside synchronized (replace it with ReentrantLock on hot paths), and in any version inside native calls. Java 24 removed the synchronized case.

Q: Do I need to rewrite my Spring application to use virtual threads? A: Usually not. On Spring Boot 3.2+ with Java 21+, setting spring.threads.virtual.enabled=true runs request handling on virtual threads. Then review concurrency limits toward databases and downstream services, since thread-pool sizes no longer throttle them.

Q: When is reactive still a better choice? A: For true streaming pipelines with backpressure (event streams, server-sent events at scale), or where the codebase and libraries are already reactive end to end. For typical request/response services with blocking I/O, virtual threads are simpler.

Advertisement

Previous

Pattern Matching

AI Tutor

Lesson: Virtual Threads

Quick actions

AI responses can be inaccurate. Verify critical information.