The 30 second answer
A platform thread is a thin wrapper over one OS thread for its whole life. It's expensive, so we pool them. A virtual thread is cheap: you make one per task and never pool it. They don't make code faster; they let many more tasks wait at once, so they help throughput for I/O-heavy work, not CPU-heavy work. In Java 21, watch out for pinning inside synchronized blocks.
| Platform thread | Virtual thread | |
|---|---|---|
| Backed by | one OS thread for its lifetime | mounted on a carrier only while running |
| Cost | large OS stack, OS scheduling | small heap-allocated stack, JDK scheduling |
| Typical count | thousands at most | can be millions |
| Pooling | yes | no, one per task |
| Good for | CPU-bound work, long-lived threads | many concurrent blocking tasks |
Pinning in Java 21: a virtual thread is pinned to its carrier when it blocks inside a synchronized block or method, or during a native call. A pinned thread keeps the carrier busy, which can hurt throughput. JEP 444 suggests replacing synchronized with ReentrantLock around frequent, long blocking operations, and -Djdk.tracePinnedThreads helps find them. Java 24 (JEP 491) removed the synchronized pinning case; native calls can still pin.
Other notes: don't pool virtual threads to limit concurrency (use a Semaphore), and be careful with large ThreadLocal caches, since there may be very many threads.
Sources: JEP 444: Virtual Threads, JEP 491: Synchronize Virtual Threads without Pinning