Home/Learn/Java A–Z/Virtual Threads (Project Loom)

Virtual Threads (Project Loom)

Advanced
Concurrency

Virtual threads (Java 21) are lightweight JVM-managed threads that make thread-per-request servers massively scalable without async code.

Overview

Virtual threads (Project Loom, finalized in Java 21) are lightweight threads managed by the JVM rather than the OS. Unlike OS threads (limited to thousands), you can create millions of virtual threads. When a virtual thread blocks on I/O, the JVM unmounts it from the carrier thread, freeing the carrier to run other virtual threads. This enables simple, readable synchronous code at the throughput of asynchronous code.

Creating Virtual Threads

Virtual threads are created with Thread.ofVirtual() or via Executors.newVirtualThreadPerTaskExecutor(). They use the same Thread API — no new programming model needed. The JVM handles mounting/unmounting on OS threads transparently.

VirtualThreads.java
// Create a single virtual thread
Thread vt = Thread.ofVirtual()
    .name("my-virtual-thread")
    .start(() -> {
        System.out.println("Running in virtual thread: "
            + Thread.currentThread().isVirtual()); // true
    });
vt.join();

// Virtual thread executor — one virtual thread per task
try (ExecutorService exec =
        Executors.newVirtualThreadPerTaskExecutor()) {

    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 10_000; i++) {
        int taskId = i;
        futures.add(exec.submit(() -> {
            Thread.sleep(100); // blocks, but doesn't tie up OS thread
            return "result-" + taskId;
        }));
    }
    // All 10,000 tasks run concurrently without 10,000 OS threads
    for (Future<String> f : futures) System.out.println(f.get());
}

Virtual Threads vs Platform Threads

Platform (OS) threads: ~1-2 MB stack, OS-managed context switch, ~10K max practical limit. Virtual threads: ~few KB stack, JVM-managed, millions practical.

Virtual threads shine for I/O-bound workloads (HTTP calls, DB queries, file I/O). They do not help CPU-bound tasks — you still need as many carrier threads as cores.

VsComparison.java
// Platform thread pool — limited by OS thread count
ExecutorService platform = Executors.newFixedThreadPool(200);

// Virtual thread executor — scales to millions
ExecutorService virtual = Executors.newVirtualThreadPerTaskExecutor();

// Benchmark: 10,000 tasks sleeping 1 second
// Platform pool (200 threads): ~50 seconds (10000 / 200)
// Virtual executor:             ~1 second  (all run concurrently)

// Virtual thread pitfalls:
// 1. Pinned threads — synchronized blocks pin the carrier
//    Fix: use ReentrantLock instead of synchronized for I/O inside
// 2. Thread-local misuse — ThreadLocal is expensive with millions of threads
//    Fix: use ScopedValue (Java 21 preview) instead

// Check if current thread is virtual
boolean isVirtual = Thread.currentThread().isVirtual();

Structured Concurrency (Java 21 Preview)

Structured Concurrency (java.util.concurrent.StructuredTaskScope) ensures that subtasks spawned by a task are scoped to that task's lifetime. If the parent task fails or is cancelled, all subtasks are cancelled too. This prevents thread leaks and simplifies error handling in fork-join style code.

StructuredConcurrency.java
import java.util.concurrent.*;

// StructuredTaskScope — parent waits for all children
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

    // Fork subtasks
    Subtask<User>  user    = scope.fork(() -> fetchUser(userId));
    Subtask<Order> order   = scope.fork(() -> fetchOrder(orderId));
    Subtask<Stock> stock   = scope.fork(() -> checkStock(itemId));

    scope.join();           // wait for all
    scope.throwIfFailed();  // propagate first failure

    // All subtasks completed successfully
    return new Response(user.get(), order.get(), stock.get());

} // scope closed — all subtasks guaranteed to be done

// ShutdownOnSuccess — take first result, cancel the rest
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
    scope.fork(() -> fetchFromPrimary());
    scope.fork(() -> fetchFromFallback());
    scope.join();
    return scope.result(); // fastest result
}

Interactive Visualization

NEWRUNNABLERUNNINGBLOCKEDWAITINGTERMINATED
synchronized(lock)— free
main
RUNNING
t1
NEW
t2
NEW
main thread creates Thread t1 and Thread t2. Both are in NEW state.
1 / 6

Key Points to Remember

  • Virtual threads are JVM-managed, lightweight (~few KB), and you can create millions.
  • When a virtual thread blocks on I/O, the JVM unmounts it — the carrier thread is freed.
  • Use Executors.newVirtualThreadPerTaskExecutor() to get one virtual thread per task.
  • synchronized blocks pin virtual threads to carrier threads — use ReentrantLock for I/O sections.
  • Structured Concurrency scopes subtask lifetimes to the parent — prevents thread leaks.

Practice Virtual Threads (Project Loom) in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What is the difference between virtual threads and platform threads?

MediumOracle
2

What does "pinning" mean for virtual threads and how do you avoid it?

HardAmazon
3

Can virtual threads improve CPU-bound performance? Why or why not?

MediumGoogle
4

What problem does Structured Concurrency solve?

HardNetflix
5

How do virtual threads change the design of a web server compared to reactive frameworks?

HardMicrosoft

Ask Aria about Virtual Threads (Project Loom)

Your personal AI tutor — ask anything about this concept