Virtual Threads (Java 21)
A virtual thread is a lightweight thread scheduled by the JVM rather than the operating system, so you can run millions of blocking tasks at once on just a handful of OS threads.
Learn Virtual Threads (Java 21) in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
You should have met basic threads and concurrency and the ExecutorService / Future API. This lesson needs Java 21 or newer — virtual threads became a final feature in Java 21.
💡 Analogy: Imagine a restaurant where every table (task) needs a dedicated waiter (platform thread) who must stand and wait while the kitchen cooks. With only a few waiters, most tables sit unserved. Virtual threads flip this: each table still gets its own waiter, but a waiter who is just waiting on the kitchen steps aside so another can take an order. The same handful of real staff (carrier OS threads) now serve thousands of tables, because nobody stands idle blocking a chair.
That is the whole pitch: keep the simple "one thread per request" code you already know, but stop paying the price of an OS thread for every request that is mostly waiting.
There are two direct ways to create one. Thread.startVirtualThread(runnable) creates and starts in a single call. Thread.ofVirtual() returns a builder you can configure (name it, set it unstarted) before starting. Both yield a normal Thread object — you still join() on it.
The headline use is Executors.newVirtualThreadPerTaskExecutor() : it spawns a brand-new virtual thread for every task you submit. Below we launch 10,000 tasks that each sleep 100ms (standing in for a network call). With platform threads, 10,000 OS threads would exhaust memory; with virtual threads, they all run on a tiny carrier pool and the whole batch finishes in well under a second.
Threads finish in an unpredictable order, so if you want repeatable output you must join all tasks (wait via future.get() or the executor's close() ), gather results in a thread-safe structure, and sort before printing. The example below does exactly that.
Answer: yes — that is precisely the scenario virtual threads are built for. A million platform threads would crash the JVM with out-of-memory, but a million mostly-sleeping virtual threads share a tiny carrier pool.
Answer: no. That work is CPU-bound and never blocks, so you are limited by the number of cores. Virtual threads help when tasks wait , not when they compute non-stop.
Answer: ExecutorService is AutoCloseable ; its close() runs an orderly shutdown that waits for every submitted task to complete before the block exits. That is why we don't need an explicit awaitTermination .
🎯 YOUR TURN — Greet in parallel
Submit one greeting task per name to a virtual-thread executor, join them all, then sort and print for deterministic output.
🧩 MINI-CHALLENGE — Parallel sum
Sum 1..1000 by splitting the range into 10 chunks, each computed on its own virtual thread as a Future<Long> , then add the partial sums.
Virtual threads make it cheap to fan out subtasks, and structured concurrency (a preview feature in Java 21, JEP 453) gives that fan-out a clean shape. With StructuredTaskScope you fork related subtasks and join them as one unit — and if one fails, the scope can cancel the rest automatically. It replaces loose collections of Future s with nested, readable structure.
You can now create virtual threads, run thousands of blocking tasks through newVirtualThreadPerTaskExecutor , explain why they scale for I/O while platform threads suit CPU work, dodge pinning, and recognise where structured concurrency fits.
Next up: The Java HTTP Client — a perfect partner for virtual threads, since HTTP calls are exactly the blocking I/O they accelerate.
Practice quiz
What is a virtual thread in Java 21?
- An OS thread with a special name
- A thread that only runs in the cloud
- A lightweight thread managed by the JVM, not the OS
- A deprecated thread type
Answer: A lightweight thread managed by the JVM, not the OS. Virtual threads are scheduled by the JVM and mapped onto a small pool of platform (OS) threads, so you can have millions of them.
Which call starts a single virtual thread immediately?
- Thread.startVirtualThread(runnable)
- new Thread().start()
- Thread.ofPlatform().start()
- Executors.newFixedThreadPool(1)
Answer: Thread.startVirtualThread(runnable). Thread.startVirtualThread(Runnable) creates and starts a virtual thread in one call.
Which executor creates a fresh virtual thread per task?
- Executors.newFixedThreadPool(n)
- Executors.newCachedThreadPool()
- Executors.newSingleThreadExecutor()
- Executors.newVirtualThreadPerTaskExecutor()
Answer: Executors.newVirtualThreadPerTaskExecutor(). newVirtualThreadPerTaskExecutor() spins up a new virtual thread for every submitted task — ideal because virtual threads are cheap.
Virtual threads shine most for which kind of workload?
- CPU-bound number crunching
- I/O-bound tasks that block a lot
- Pure in-memory sorting
- GPU rendering
Answer: I/O-bound tasks that block a lot. When a virtual thread blocks on I/O it is unmounted, freeing its carrier so other virtual threads run. CPU-bound work is still limited by your cores.
How many platform (OS) threads back many blocked virtual threads?
- A small pool of carrier threads
- One per virtual thread
- Exactly two
- Zero
Answer: A small pool of carrier threads. Many virtual threads share a small pool of carrier (platform) threads; a blocked virtual thread releases its carrier.
What happens when an ExecutorService from a try-with-resources block closes?
- It cancels all tasks
- It throws an exception
- It waits for all submitted tasks to finish
- Nothing
Answer: It waits for all submitted tasks to finish. ExecutorService implements AutoCloseable; close() performs an orderly shutdown that blocks until tasks complete.
How do you check if a Thread is virtual?
- thread.isLight()
- thread.isVirtual()
- thread.getType()
- thread.virtual
Answer: thread.isVirtual(). Thread.isVirtual() returns true for virtual threads and false for platform threads.
Why should you avoid pooling virtual threads?
- They cannot be pooled
- Pooling corrupts them
- The JVM forbids it
- They are so cheap to create that pooling adds no value
Answer: They are so cheap to create that pooling adds no value. Virtual threads are cheap to create and discard, so the classic 'reuse expensive threads' rationale for pools disappears — create one per task.
What can pin a virtual thread to its carrier (preventing unmount)?
- A simple loop
- Blocking inside a synchronized block or a native call
- Returning a value
- Calling join()
Answer: Blocking inside a synchronized block or a native call. Blocking while holding a monitor (synchronized) or inside native code can pin the virtual thread, tying up its carrier. Prefer ReentrantLock in hot paths.
What is Structured Concurrency (preview) for?
- Sorting threads
- Replacing virtual threads
- Treating a group of related subtasks as one unit with shared cancellation
- Drawing thread diagrams
Answer: Treating a group of related subtasks as one unit with shared cancellation. StructuredTaskScope groups subtasks so they start, join, and cancel together — a cleaner model than scattered futures.