BackendBench
Coffee

Unravel โ€บ Java concepts โ€บ Concurrency

Concurrency

Virtual threads

A virtual thread is a very cheap thread managed by the JVM instead of the operating system.

Learn these first

At a glance

Mental model

Instead of a few expensive full-time employees who stand idle while waiting on the phone, you have an unlimited pool of temporary staff who are only paid (use a real worker) while actually talking.

What happens, in order
  1. 1Thread.startVirtualThread(runnable), Thread.ofVirtual().name("job").start(r) or Executors.newVirtualThreadPerTaskExecutor() (one new virtual thread per task, never pooled)
  2. 2Code stays plain blocking code (inputStream.read(), JDBC, HttpClient.send)When it blocks, the JVM unmounts the virtual thread from its carrier (a small pool of platform threads) and lets another run.
  3. 3They are for I/O-bound concurrency (many waiting requests), not for CPU-heavy work, where only as many threads as cores help
  4. 4Spring Boot 3.2+ enables them with spring.threads.virtual.enabled=true
Remember
Watch out
Pooling virtual threadsUsing them for CPU-bound loopsHolding a synchronized monitor while doing blocking I/OAssuming ThreadLocal data is free
Example (highlighted lines explain it)
import java.util.concurrent.*;import java.util.List;public class Fetcher {  static void fetchAll(List<String> urls) throws Exception {    try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {      for (String u : urls) exec.submit(() -> { Thread.sleep(100); return u.length(); });    }  }  static Thread one(Runnable r) { return Thread.startVirtualThread(r); }}
  1. line 5 uses newVirtualThreadPerTaskExecutor, the core of Virtual threads
  2. line 9 uses startVirtualThread, the core of Virtual threads
Read the full explanation (about 1 min)

A virtual thread is a very cheap thread managed by the JVM instead of the operating system. You can start a million of them, so a simple "one thread per request" blocking style scales again.

A mental model

Instead of a few expensive full-time employees who stand idle while waiting on the phone, you have an unlimited pool of temporary staff who are only paid (use a real worker) while actually talking.

How it works

Thread.startVirtualThread(runnable), Thread.ofVirtual().name("job").start(r) or Executors.newVirtualThreadPerTaskExecutor() (one new virtual thread per task, never pooled).

Code stays plain blocking code (inputStream.read(), JDBC, HttpClient.send). When it blocks, the JVM unmounts the virtual thread from its carrier (a small pool of platform threads) and lets another run.

They are for I/O-bound concurrency (many waiting requests), not for CPU-heavy work, where only as many threads as cores help.

Spring Boot 3.2+ enables them with spring.threads.virtual.enabled=true.

Going deeper

Do not pool virtual threads; create one per task. Limit concurrency of a scarce resource (database connections) with a Semaphore, not with the thread count.

synchronized blocks that perform blocking I/O pinned the carrier thread in Java 21; prefer ReentrantLock there, or use a newer JDK where this is fixed.

ThreadLocals still work but cost memory per virtual thread. StructuredTaskScope (preview) treats a group of subtasks as one unit with clean cancellation.

Under the hood

A virtual thread's stack is stored on the heap as a continuation. On a blocking call the continuation yields, its frames are copied away and the carrier thread (from a ForkJoinPool scheduler) takes the next virtual thread. Resuming copies the frames back.

Common mistakes

  • Pooling virtual threads.
  • Using them for CPU-bound loops.
  • Holding a synchronized monitor while doing blocking I/O.
  • Assuming ThreadLocal data is free.

How Unravel recognises it

When you paste code, Unravel looks for these signs of virtual threads. Spotting them yourself is a good habit when you read unfamiliar code:

Part of a learning path

Related concepts

Frequently asked questions

What is Virtual threads?

A virtual thread is a very cheap thread managed by the JVM instead of the operating system. You can start a million of them, so a simple "one thread per request" blocking style scales again.

What are the most common mistakes with Virtual threads?

Pooling virtual threads. Using them for CPU-bound loops. Holding a synchronized monitor while doing blocking I/O. Assuming ThreadLocal data is free.

How can I see Virtual threads in real code?

Paste your own Java into Unravel and it will point out where virtual threads appears, explain how it works at the detail level you choose, and draw the flow of the code around it.