Skip to main content

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:

  1. CPU-bound work gains nothing: you still have only the machine's cores; Runtime.getRuntime().availableProcessors() governs platform parallelism. VTs help waiting, not computing.
  2. Pinning: inside a synchronized block (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 the synchronized case; on our --release 21 runtime it is a live hazard โ€” prefer ReentrantLock for long sections in VT-heavy code.)
  3. 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.

Now practice

Executor & VT drillsDrive the lifecycle, compose without blocking, and fan out 10k virtual tasks within sandbox limits.3 challenges ยท ยท ~55 min