Mastering Java 21 Virtual Threads

A Complete Developer's Guide for 2025

Oct 2025 ยท 15 min read

1. Introduction

Java has always been a pioneer in concurrency, evolving from simple thread APIs to advanced constructs like CompletableFuture and reactive programming frameworks. However, traditional Java concurrency relies on platform threads, which are mapped one-to-one with underlying OS threads.

Java 21 introduces virtual threads, a revolutionary concurrency model designed to address scalability challenges. Virtual threads are lightweight, user-mode threads managed by the JVM rather than the OS, enabling massive concurrency with minimal resources.

For developers, virtual threads mean writing simpler, more intuitive concurrent code without sacrificing scalability or performance.

2. Understanding Java 21 Virtual Threads

2.1 What Are Virtual Threads?

Virtual threads are lightweight threads managed by the JVM, not the operating system. Unlike platform threads that map 1:1 to OS threads, virtual threads are multiplexed onto a smaller pool of OS threads called "carrier threads".

Visual Architecture:

๐Ÿ”ด Platform Threads (Traditional)

Java Thread
โ†’
OS Thread
โ†’
CPU Core
1:1 Mapping (One Java thread = One OS thread)
Memory per thread: ~1 MB
Max threads: ~5,000-10,000
Creation time: ~1-2 ms

๐ŸŸข Virtual Threads (Java 21)

VThread 1
VThread 2
VThread N
Millions of virtual threads...
โ†“
Carrier Threads Pool
โ†“
OS Threads
M:N Mapping (Many virtual threads โ†’ Few OS threads)
Memory per thread: ~few KB
Max threads: Millions
Creation time: ~1-10 ฮผs

Key Terminology:

  • Platform Threads: Traditional Java threads mapped to OS threads (1:1 mapping).
  • Virtual Threads: Lightweight JVM-managed threads (M:N mapping).
  • Carrier Threads: OS threads that execute virtual threads (default pool size = number of CPU cores).
  • Project Loom: The OpenJDK project that introduced virtual threads.

2.2 Key Advantages

  • Massive Scalability: Handle millions of concurrent tasks instead of thousands.
  • Simplified Programming Model: Write straightforward blocking code that scales like async.
  • Better Resource Utilization: Efficient CPU usage with automatic thread management.
  • Backward Compatible: Works with existing Thread API and libraries.

Detailed Comparison:

Feature Platform Threads Virtual Threads
Memory per thread ~1 MB (stack) ~few KB
Max threads ~5,000-10,000 Millions
Creation time ~1-2 ms ~1-10 ฮผs (1000x faster)
Context switching OS-level (slow) JVM-level (fast)
Blocking behavior Blocks OS thread Unmounts from carrier
Use case CPU-bound tasks I/O-bound tasks
Thread pool needed Yes (limited) No (create per-task)
Backward compatible N/A Yes (Thread API)

2.3 Code Examples

Example 1: Creating a Platform Thread

Thread platformThread = new Thread(() -> { System.out.println("Running on a platform thread"); }); platformThread.start();

Example 2: Creating a Virtual Thread (Java 21+)

Thread virtualThread = Thread.startVirtualThread(() -> { System.out.println("Running on a virtual thread"); }); virtualThread.join();

Example 3: Memory Comparison

System.out.println("Platform thread: ~1MB stack"); System.out.println("Virtual thread: ~few KB stack");

๐Ÿ’ป Run This Example

Complete working code with tests available on GitHub: BasicVirtualThreadExample.java

โœ… Best Practices

  • Use virtual threads primarily for blocking or I/O-bound tasks.
  • Avoid mixing virtual threads with thread-local state.
  • Always join or manage thread lifecycles properly.
  • Profile applications to understand actual resource usage.

3. Getting Started with Java 21 Virtual Threads

3.1 Setting Up Your Environment

To start using virtual threads, you need JDK 21 or newer. Popular IDEs like IntelliJ IDEA and Eclipse already support this version. Ensure your project SDK is set accordingly.

3.2 Creating and Managing Virtual Threads

Java 21 introduces convenient methods on Thread to create virtual threads:

  • Thread.startVirtualThread(Runnable)
  • Thread.ofVirtual()

Example 1: Simple Virtual Thread Creation

Thread vt1 = Thread.startVirtualThread(() -> { System.out.println("Virtual thread running"); }); vt1.join();

Example 2: Using Thread.ofVirtual() for Custom Configuration

Thread vt2 = Thread.ofVirtual() .name("my-virtual-thread") .start(() -> { System.out.println("Named virtual thread"); }); vt2.join();

๐Ÿ’ป Run This Example

Complete working code with tests available on GitHub: VirtualThreadExecutorExample.java

4. Advanced Concepts and Best Practices

4.1 Using Virtual Thread Executors

Virtual threads integrate smoothly with Executors via the new Executors.newVirtualThreadPerTaskExecutor().

Example: Virtual Thread Executor Service

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); executor.submit(() -> { System.out.println("Running task in virtual thread executor"); }); executor.shutdown();

4.2 Handling Exceptions

Virtual threads behave like platform threads regarding exceptions. Use proper uncaught exception handlers and try/catch blocks.

Example: Exception Handling in Virtual Threads

Thread vt = Thread.startVirtualThread(() -> { try { throw new RuntimeException("Oops"); } catch (Exception e) { System.err.println("Caught: " + e.getMessage()); } }); vt.join();

โš ๏ธ Common Pitfalls

  • Forgetting to shut down executors causes resource leaks.
  • Ignoring exceptions in async tasks leads to silent failures.
  • Blocking calls inside reactive pipelines degrade performance.

5. Real-World Applications

5.1 Building Scalable Web Servers

Virtual threads transform server-side programming by allowing synchronous-style request handling scaled to millions of connections.

Example 1: Simple Virtual Thread-Based HTTP Server

HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); server.createContext("/", exchange -> { Thread.startVirtualThread(() -> { try { String response = "Hello from virtual thread server!"; exchange.sendResponseHeaders(200, response.getBytes().length); exchange.getResponseBody().write(response.getBytes()); exchange.close(); } catch (Exception e) { e.printStackTrace(); } }); }); server.start();

๐Ÿข Production Story: Oracle Helidon 4.0

Oracle's Helidon framework adopted virtual threads and achieved:

  • 90% memory reduction in high-concurrency scenarios.
  • 5x throughput increase for I/O-bound workloads.
  • Simplified codebase: Replaced reactive code with blocking I/O.
  • Production scale: Millions of requests/day with lower costs.

Source: Oracle Helidon 4.0 Release Notes, 2023

๐Ÿ’ป Run This Example

Complete working code with tests available on GitHub: ScalabilityDemo.java

6. Challenges and Limitations

6.1 Current Limitations in Java 21

  • Native blocking calls can block carrier threads.
  • Some APIs are not fully optimized for virtual threads.
  • Debugging tools are still evolving.
  • Thread-local variables can behave unexpectedly.

6.2 When to Use vs When NOT to Use

Decision Matrix:

Scenario Platform Threads Virtual Threads Reason
REST API server โŒ โœ… High I/O, many concurrent requests
Database connection pool โŒ โœ… Blocking I/O operations
File processing (I/O) โŒ โœ… Waiting on disk I/O
CPU-intensive calculations โœ… โŒ Need dedicated CPU time
Real-time trading systems โœ… โŒ Require predictable scheduling
Image/video processing โœ… โŒ CPU-bound, no blocking
Microservices communication โŒ โœ… Network I/O, high concurrency
Machine learning inference โœ… โŒ CPU/GPU intensive
WebSocket connections (1M+) โŒ โœ… Mostly idle, waiting for messages
Batch ETL jobs โŒ โœ… I/O bound, parallel processing

โš ๏ธ When NOT to Use Virtual Threads

  • CPU-bound computations: Matrix multiplication, cryptography, compression.
  • Real-time systems: Trading platforms, industrial control systems.
  • Heavy ThreadLocal usage: Legacy code with extensive thread-local state.
  • Native blocking calls: JNI calls that block the carrier thread.
  • Synchronized blocks on shared objects: Can pin carrier threads.

๐Ÿข More Production Success Stories

Spring Framework 6.1+

  • Added virtual thread support in Spring Boot 3.2.
  • Companies report 3-4x throughput improvements.
  • Simplified reactive code back to imperative style.

Quarkus Framework

  • Native virtual thread integration since v3.2.
  • Reduced memory footprint by 80% in microservices.
  • Handles 100K+ concurrent connections per instance.

Apache Tomcat 10.1+

  • Virtual thread executor support added.
  • Benchmark: 50K concurrent users with 2GB RAM (vs 16GB with platform threads).
  • p95 latency reduced by 45%.

โš ๏ธ Important Considerations

  • Avoid blocking native calls inside virtual threads.
  • Minimize thread-local usage with virtual threads.
  • Stay updated with JVM releases and improvements.
  • Profile applications to detect blocking hotspots.

7. Conclusion

Java 21 virtual threads represent a major leap forward in concurrency, simplifying how developers write scalable, efficient, and maintainable concurrent Java applications. By understanding their fundamentals and applying best practices, you can unlock significant performance and productivity gains.

While virtual threads are not a panacea, their benefits for I/O-bound and high-concurrency workloads are profound. Adopting virtual threads today prepares your applications for a scalable future and reduces complexity associated with asynchronous programming.

Start experimenting with virtual threads in your next project! Refactor blocking code, try virtual thread executors, and measure the improvements.

๐Ÿ’ป Try It Yourself - Interactive Java Playground

Experiment with virtual threads directly in your browser:

๐Ÿ’ก Tip: Copy any example from this tutorial and paste it into the playground above to run it instantly!

All examples from this tutorial are available with full tests on GitHub.

8. Further Learning Resources

Explore these additional resources to deepen your understanding of Java 21 virtual threads:

๐Ÿ’ป Try It Yourself