DEV Community

Cover image for ๐Ÿ’กJava Virtual Threads Explained: Continuations, Carrier Threads, and Scalable Concurrency
24adithya
24adithya

Posted on

๐Ÿ’กJava Virtual Threads Explained: Continuations, Carrier Threads, and Scalable Concurrency

๐Ÿ“Œ Introduction

Java has long been a reliable language for concurrent programming. But for I/O-heavy, high-concurrency systems, traditional OS thread-based models introduced scaling limits.
Virtual Threads (from Java 21โ€™s Project Loom) revolutionize this by allowing 10,000+ lightweight concurrent threads โ€” without the headaches of thread pool sizing or async callbacks.

In this post, Iโ€™ll explain what Virtual Threads are, how they work internally with continuations, and where you should โ€” and shouldnโ€™t โ€” use them. Iโ€™ll also show you how to write and benchmark them with real code.

๐Ÿ“ What Are Virtual Threads?

A virtual thread is a lightweight user-space thread managed by the JVM. When a virtual thread performs a blocking operation (like a file read or HTTP call), the JVM parks the thread and frees up the underlying OS thread to handle other work.

When the blocking operation completes, the virtual thread is unparked and resumed by any available OS thread.

๐Ÿ“ How To Spawn Virtual Threads

Creating virtual threads is as easy as using Thread.ofVirtual():

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); executor.submit(() -> System.out.println("Task in virtual thread")); executor.shutdown(); 
Enter fullscreen mode Exit fullscreen mode

Or use an ExecutorService:

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); executor.submit(() -> System.out.println("Task in virtual thread")); executor.shutdown(); 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ Carrier Threads: The Backbone Behind Virtual Threads

While virtual threads themselves are lightweight and managed by the JVM, they still need to run on actual OS threads when active. These are called carrier threads.

When a virtual thread performs work:

  • Itโ€™s picked up by a carrier thread (native OS thread) from a small internal ForkJoinPool.
  • If the virtual thread blocks on I/O:
  • The JVM parks the virtual thread (saves its continuation).
  • The carrier thread is freed up for other work.
  • When the I/O operation completes:
  • The virtual thread is unparked.
  • Scheduled on any available carrier thread to resume execution. Key point: A virtual thread is not bound to a specific carrier thread. It can run on any available carrier thread when ready โ€” enabling extreme concurrency with minimal OS thread usage.

๐Ÿ“Œ Where Do Carrier Threads Come From?

By default:

The JVM uses a ForkJoinPool internally to manage these carrier threads.

Its size is typically equal to Runtime.getRuntime().availableProcessors() but can be configured via:

-Djdk.virtualThreadScheduler.parallelism=16 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Œ Benchmark: OS Threads vs Virtual Threads

Letโ€™s see how they perform for an I/O-heavy task like making multiple HTTP requests.

Benchmark Code:

import java.net.URI; import java.net.http.; import java.util.concurrent.; import java.util.stream.IntStream; public class ThreadBenchmark { static final int TASK_COUNT = 1000; public static void main(String[] args) throws Exception { System.out.println("Benchmarking with OS Threads..."); benchmarkWithExecutor(Executors.newFixedThreadPool(100)); System.out.println("\nBenchmarking with Virtual Threads..."); benchmarkWithExecutor(Executors.newVirtualThreadPerTaskExecutor()); } private static void benchmarkWithExecutor(ExecutorService executor) throws Exception { var client = HttpClient.newHttpClient(); var start = System.currentTimeMillis(); var futures = IntStream.range(0, TASK_COUNT) .mapToObj(i -> executor.submit(() -> { try { var request = HttpRequest.newBuilder(URI.create("https://httpbin.org/get")).build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(Thread.currentThread() + " got response " + i); } catch (Exception e) { e.printStackTrace(); } })) .toList(); for (var future : futures) { future.get(); } var end = System.currentTimeMillis(); System.out.println("Time taken: " + (end - start) + " ms"); executor.shutdown(); } } 
Enter fullscreen mode Exit fullscreen mode



โšก Try changing TASK_COUNT to 10,000 โ€” youโ€™ll notice virtual threads handling it gracefully, while OS thread pools may choke or exhaust memory.

๐Ÿ“ How Virtual Threads Work (Continuations Behind the Scenes)

When a virtual thread blocks:

The JVM captures its continuation โ€” the current call stack, local variables, and program counter.

The virtual thread is parked (suspended)

The underlying OS thread is released for other tasks

Once the blocking operation completes, the virtual thread is unparked and resumed โ€” potentially on a different OS thread.

This is made possible by jdk.internal.vm.Continuation, which acts like a bookmark in the threadโ€™s execution flow.

๐Ÿ“ Similarity to Other Languages

Language Mechanism
Go Goroutines
Kotlin Coroutines
Python AsyncIO + async/await
JavaScript Async/await + microtask queue

Java 21+ finally joins this club with a clean, native solution.

๐Ÿ“ When to Use Virtual Threads โœ…

  • HTTP servers
  • Database access
  • Microservices
  • File and network I/O
  • High concurrency workloads with blocking calls

๐Ÿ“ When Not to Use โŒ

  • CPU-bound computations
  • (Use a FixedThreadPool sized to available cores)
  • Native blocking code via JNI โ€” still blocks OS thread
  • Not a replacement for parallel processing

๐Ÿ“ Recap Table

Feature OS Threads Virtual Threads
Backed by OS kernel thread Lightweight, JVM-managed
Blocking call Ties up OS thread Parks virtual thread
CPU-bound parallelism Excellent Same as OS threads
I/O concurrency Limited 10k+ scalable threads
Scheduling OS kernel JVM continuation scheduling

๐Ÿ“Œ Final Thought

Virtual Threads make asynchronous-like scalability achievable with simple, blocking code in Java. Project Loom modernizes concurrency without losing Javaโ€™s type safety or predictability.

If you build server-side or networked Java apps โ€” this is the future.

๐Ÿ’ฌ Would love to hear your thoughts โ€” drop a comment or share your experience with Virtual Threads!

Top comments (0)