Virtual threads and their limits
advanced18 min readLesson 139 of 180
Millions of cheap threads, carriers and unmounting, pinning hazards, and CPU-bound work unchanged.
Virtual threads (JEP 444, standard in 21 โ verified executable in this sandbox) detach unit of concurrency from unit of cost:
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
exec.submit(() -> { Thread.sleep(100); return i; }); // fine!
}
} // close() waits for all tasks
One JVM can hold millions of virtual threads because they are not OS threads: they are heap objects scheduled onto a small pool of carrier platform threads. When a virtual thread blocks (sleep, I/O), the JVM unmounts it and the carrier runs someone else. Blocking is cheap again โ that is the whole revolution.
What virtual threads do NOT change:
- CPU-bound work gains nothing: you still have only the machine's cores;
Runtime.getRuntime().availableProcessors()governs platform parallelism. VTs help waiting, not computing. - Pinning: inside a
synchronizedblock (or a native frame), a virtual thread cannot unmount โ the carrier is held hostage. Long synchronized sections under heavy VT load can starve carriers. (JDK 24 fixed thesynchronizedcase; on our--release 21runtime it is a live hazard โ preferReentrantLockfor long sections in VT-heavy code.) - No pooling: VTs are one-shot. Pooling them recreates the platform-thread
economics you were escaping.
newVirtualThreadPerTaskExecutorโ per task.
Structured concurrency (JEP 453) is preview-only in 21 โ taught conceptually: treat a fan of related tasks as one unit with one error and one cancellation path.