Review: Virtual Threads & Loom

[!NOTE] This module explores the core principles of Review: Virtual Threads & Loom, deriving solutions from first principles and hardware constraints to build world-class, production-ready expertise.

1. Key Takeaways

  1. M:N Scheduling: Virtual Threads are user-mode threads scheduled by the JVM onto a small pool of OS “Carrier Threads”.
  2. Blocking is Cheap: When a virtual thread blocks (e.g., I/O), it unmounts from the carrier thread, allowing the carrier to execute other work.
  3. No Pooling: Never pool virtual threads. Create them on-demand (one per task).
  4. Structured Concurrency: Use StructuredTaskScope to treat a group of related subtasks as a single unit, ensuring clean error propagation and cancellation.
  5. Scoped Values: Use ScopedValue instead of ThreadLocal for immutable, bounded data sharing across stack frames.

2. Flashcards

Platform Thread vs Virtual Thread

(Click to flip)

Platform Thread: 1:1 map to OS thread, expensive (~2MB stack), slow context switch. Virtual Thread: M:N map to OS thread, cheap (heap stack), fast context switch.

Carrier Thread

The actual OS thread (Platform Thread) that executes the Virtual Thread. The JVM "mounts" VTs onto Carriers.

StructuredTaskScope.ShutdownOnFailure()

A scope policy where if any subtask fails, all other running subtasks are cancelled, and the exception is propagated.

Why avoid ThreadLocal with VTs?

Because VTs are numerous (millions). ThreadLocal maps are mutable, unbounded, and expensive to inherit/copy for every new thread.

What is "Pinning"?

When a VT executes inside a synchronized block or native method, it gets "pinned" to the Carrier Thread, preventing unmounting during blocking ops.

Correct Executor for VTs

Executors.newVirtualThreadPerTaskExecutor(). Never use a fixed thread pool for VTs.

3. Cheat Sheet

Virtual Threads

// 1. Executor (Best Practice)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> process(data));
}

// 2. Builder
Thread.ofVirtual().name("worker").start(task);

// 3. Static Factory
Thread.startVirtualThread(task);

Structured Concurrency

// ShutdownOnFailure (All-or-Nothing)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var taskA = scope.fork(() -> serviceA.call());
    var taskB = scope.fork(() -> serviceB.call());

    scope.join();          // Wait
    scope.throwIfFailed(); // Propagate exception

    return new Result(taskA.get(), taskB.get());
}

Scoped Values

// Define
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

// Bind & Run
ScopedValue.where(CURRENT_USER, user).run(() -> {
    // Access
    process(CURRENT_USER.get());
});

Common Pitfalls (The “Don’ts”)

Anti-Pattern Why? Fix
Pooling VTs VTs are cheap/disposable. Pooling creates overhead. Create newVirtualThreadPerTaskExecutor().
synchronized Pins the thread to the carrier, blocking the OS thread. Use ReentrantLock instead.
ThreadLocal High memory usage, expensive inheritance. Use ScopedValue.
CPU Heavy Tasks VTs are for I/O. CPU tasks hog carriers. Use Platform Threads (ForkJoinPool).

Java Glossary