How Java works

Pick a topic, write code and watch what happens inside, step by step.

Memory

Collections

Concurrency

Language and JVM

Topics

Virtual threads

Platform threads are expensive

A normal Thread is an operating system thread. It has its own stack, and creating it and switching between threads costs a lot.

That is why servers use thread pools. But a pooled thread that waits for a response does nothing. With 200 threads, only 200 requests can wait at the same time.

Java 21 added virtual threads. They are the JVM’s own threads, and you can create millions of them.

Mount and unmount

A virtual thread does not run on the CPU by itself. A few platform threads run it. They are called carriers, and they live in a ForkJoinPool.

When a virtual thread waits on sleep, a socket or a lock, it unmounts from its carrier: its stack moves to the heap and the carrier takes the next virtual thread.

When the wait ends, the virtual thread goes back to the queue and continues on any free carrier. Switch the mode and compare the times.

Pinning

In Java 21 a virtual thread cannot unmount inside synchronized, because the monitor belongs to the carrier thread. This is called pinning: while it waits, the carrier waits too.

The same happens inside a native method. -Djdk.tracePinnedThreads=short shows where it happens.

In Java 21 the fix is ReentrantLock. Since Java 24 (JEP 491), synchronized no longer pins.

Where they do not help

Virtual threads make waiting cheap, but they do not make computation faster. For CPU work, one thread per core is enough.

They are just cheap threads. To limit how much runs at once, use a Semaphore, not a pool.