Study guide
9.5 Virtual Threads in Java 21 (Carrier Threads, Pinning, and Best Practices)
Oracle Java SE 21 Developer 1Z0-830
Track your progress as you read
Finished sections get a check mark.
1 Chapter 1: Java Basics, Primitive Types, and Operators
1.1 Program Structure, Primitives, Literals, Scopes, and var 1.2 Operators, Precedence, and Type Casting 1.3 Strings, Text Blocks, and StringBuilder 1.4 Date and Time APIs (java.time)
2 Chapter 2: Controlling Program Flow & Pattern Matching
2.1 Conditional Logic: if-else and Ternary Operators 2.2 Switch Statements and Switch Expressions 2.3 Pattern Matching for switch and instanceof 2.4 Loops and Branching Statements
3 Chapter 3: Object-Oriented Design & Class Architecture
3.1 Class Declarations, Constructors, and Object Lifecycle 3.2 Encapsulation and Access Modifiers 3.3 Inheritance, Method Overriding, and Polymorphism 3.4 Abstract Classes and Interfaces 3.5 Nested, Inner, Local, and Anonymous Classes 3.6 Method Overloading, Overload Resolution, and Varargs
4 Chapter 4: Modern Java Data Modeling: Records, Enums, and Sealed Types
4.1 Java Records, Canonical/Compact Constructors, and Component Accessors 4.2 Java Enums: Fields, Methods, and Abstract Constant-Specific Behavior 4.3 Sealed Classes and Interfaces: permits, final, non-sealed, and Hierarchy Architecture 4.4 Advanced Record Patterns (JEP 440), Nested Deconstruction, and Sealed Hierarchy Exhaustiveness
5 Chapter 5: Exception Handling and Annotations
5.1 Exception Hierarchy: Checked vs. Unchecked Exceptions 5.2 Exception Handling: try, catch, finally, and Multi-Catch 5.3 Resource Management: try-with-resources and AutoCloseable 5.4 Custom Exceptions and Best Practices 5.5 Java Annotations: Built-in, Meta-Annotations, and Custom Types
6 Chapter 6: Generics and the Collections Framework
6.1 Generics Fundamentals and Generic Methods 6.2 Wildcards, Type Bounds, and Type Erasure 6.3 Core Collections: Lists, Sets, Queues, and Maps 6.4 Sequenced Collections in Java 21 6.5 Sorting, Comparable, and Comparator 6.6 Arrays: Creation, Multidimensional Forms, and the Arrays Utility Class
7 Chapter 7: Functional Programming & Lambda Expressions
7.1 Built-in Functional Interfaces in java.util.function 7.2 Lambda Expressions, Variable Scopes, and Effectively Final 7.3 Method References: Four Varieties and Edge Cases 7.4 Working with java.util.Optional
8 Chapter 8: The Stream API and Data Pipelines
8.1 Stream Creation and Intermediate Operations 8.2 Terminal Operations and Reductions 8.3 Advanced Collectors, Grouping, Partitioning, and Teeing 8.4 Primitive Streams and Parallel Stream Processing
9 Chapter 9: Concurrency, Multithreading, and Virtual Threads
9.1 Thread Fundamentals, Creation, and Lifecycle 9.2 Thread Safety, Synchronization, and the Java Memory Model 9.3 Concurrency Utilities: ExecutorService, Callable, and Future 9.4 Concurrent Collections, Atomics, and Explicit Locks 9.5 Virtual Threads in Java 21 (Carrier Threads, Pinning, and Best Practices)
10 Chapter 10: The Java Platform Module System (JPMS)
10.1 Module Architecture and module-info.java Declarations 10.2 Module Directives: requires, exports, opens, provides, and uses 10.3 Modular Compilation, Packaging, and CLI Execution 10.4 Module Types, Migration Strategies, ServiceLoader SPI, and JLink
11 Chapter 11: Java I/O, NIO.2, and Object Serialization
11.1 Classic Java I/O Streams and Readers/Writers 11.2 Java NIO.2 File API: Path, Paths, and File System Operations 11.3 Advanced NIO.2 Stream Operations and Directory Traversal 11.4 Java Object Serialization and Record Serialization
12 Chapter 12: Implementing Localization and Internationalization
12.1 Locales and Resource Bundles 12.2 Formatting and Parsing Numbers, Currencies, Dates, Times, and Messages
9.5 Virtual Threads in Java 21 (Carrier Threads, Pinning, and Best Practices)
Key Takeaways Explain This Topic AI
Virtual threads (JEP 444) are lightweight, JVM-managed user-mode threads scheduled M:N on top of an internal ForkJoinPool carrier thread pool, scaling to millions of concurrent tasks.
Virtual threads are permanently daemon threads with fixed normal priority (5); invoking setDaemon(false) throws IllegalArgumentException, while setPriority() is silently ignored.
Thread pinning occurs when a virtual thread blocks inside a synchronized block/method or native call, preventing unmounting from its carrier thread and degrading system throughput.
Refactoring synchronized blocks in I/O paths to java.util.concurrent.locks.ReentrantLock eliminates thread pinning because ReentrantLock unmounts cleanly via LockSupport.park().
Virtual threads should never be pooled; applications should create a new virtual thread per task using Thread.ofVirtual() or Executors.newVirtualThreadPerTaskExecutor() while keeping platform thread pools for CPU-bound computations.
Last updated: September 2026
Virtual Threads in Java 21 (Carrier Threads, Pinning, and Best Practices)
Finalized as part of Project Loom in Java SE 21 (JEP 444) , Virtual Threads represent the most significant architectural evolution of Java concurrency since Java 5. Virtual threads solve the thread scalability bottleneck by decoupling Java's thread abstraction from operating system kernel threads, enabling high-throughput concurrent applications written in the simple, synchronous, thread-per-request style.
1. The Thread Scalability Bottleneck: Platform vs. Virtual Threads
Historically, every java.lang.Thread was a Platform Thread (a thin wrapper around an OS kernel thread). This 1:1 architecture introduces severe limitations:
- Attribute Platform Thread (Kernel Thread) Virtual Thread (JEP 444 / Java 21)
- Mapping to OS 1:1 mapping with an OS kernel thread M:N multiplexing over carrier platform threads
Memory Footprint Large: ≈ 1 MB \approx 1\,\text{MB} ≈ 1 MB reserved call stack memory Tiny: ≈ 1 KB \approx 1\,\text{KB} ≈ 1 KB metadata in Java heap memory
Creation Cost Expensive (requires OS kernel context allocation) Inexpensive (simple Java object allocation on heap)
Max Capacity Few thousands ( ≈ 2 , 000 – 10 , 000 \approx 2,000\text{--}10,000 ≈ 2 , 000 – 10 , 000 per JVM) Millions of active concurrent threads
- Context Switch Heavy (OS kernel mode transition, registers, CPU) Lightweight (JVM user-mode continuation jump)
- Blocking Cost Blocks underlying OS kernel thread Unmounts from carrier thread; OS thread remains free
- Ideal Workload CPU-intensive computational tasks High-throughput, I/O-bound blocking workloads
- 2. Carrier Threads and M:N Scheduling
Virtual threads are executed by mounting them onto underlying platform threads known as Carrier Threads :
Carrier Pool : The JVM manages carrier threads using an internal FIFO ForkJoinPool .
Default Parallelism : The number of carrier threads defaults to Runtime.getRuntime().availableProcessors() (configurable via -Djdk.virtualThreadScheduler.parallelism=N ).
Mounting & Continuation Mechanism :
When a virtual thread executes code, it is mounted onto a carrier platform thread.
When the virtual thread encounters a blocking operation (e.g., SocketChannel.read() , Thread.sleep() , BlockingQueue.take() , LockSupport.park() ), the virtual thread yields its continuation.
Its execution stack frames are copied from the carrier stack into the JVM heap , and the virtual thread unmounts .
The carrier thread is immediately released to mount and execute another runnable virtual thread.
When the blocking operation completes in the OS kernel, the JVM schedules the virtual thread continuation, mounting it on any available carrier thread (not necessarily the original one). +-------------------------------------------------------------------------+
- | MILLIONS OF VIRTUAL THREADS |
- | [VT 1: Active] [VT 2: Blocked on I/O] [VT 3: Active] [VT 4...] |
- +-------------------------------------------------------------------------+
- | | (Unmounted) |
- (Mounted) v (Mounted)
- +-------------------+ +-------------------+ +-------------------+
- | Carrier Thread 1 | | Carrier Thread 2 | | Carrier Thread 3 | (ForkJoinPool)
- | (OS Thread 1) | | (OS Thread 2) | | (OS Thread 3) |
- +-------------------+ +-------------------+ +-------------------+
- 3. Creating and Launching Virtual Threads
- Java 21 provides several APIs to instantiate virtual threads: // Method 1: Instant launch helper
- Thread vt1 = Thread.startVirtualThread(() -> {
- System.out.println("Running in VT: " + Thread.currentThread());
- });
- // Method 2: Fluent Builder API (Configured and started)
- Thread vt2 = Thread.ofVirtual()
.name("order-worker-", 0) // Names: order-worker-0, order-worker-1...
.start(() -> System.out.println("Named VT running"));
- // Method 3: Fluent Builder unstarted
- Thread vt3 = Thread.ofVirtual()
- .name("batch-vt")
- .unstarted(() -> System.out.println("Unstarted VT"));
- vt3.start();
- // Method 4: ThreadFactory integration
- ThreadFactory factory = Thread.ofVirtual().name("http-req-", 1).factory();
- Thread vt4 = factory.newThread(() -> System.out.println("Factory created VT"));
- vt4.start();
- // Method 5: ExecutorService (One virtual thread per submitted task)
- try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
- for (int i = 0; i < 10_000; i++) {
- int taskId = i;
- executor.submit(() -> {
- Thread.sleep(1000); // Unmounts cleanly; does not block OS thread
- return taskId;
- });
- }
- } // Auto-closes: shuts down and awaits all 10,000 tasks
- 4. Invariants and Architectural Rules for the Exam
Virtual threads possess strict invariants that differ fundamentally from platform threads:
Permanent Daemon Status :
Virtual threads are always daemon threads . The JVM will never wait for virtual threads to finish when all non-daemon platform threads have terminated.
Calling vt.setDaemon(false) throws java.lang.IllegalArgumentException .
Fixed Normal Priority :
Virtual threads always have Thread.NORM_PRIORITY (5).
Calling vt.setPriority(int priority) is silently ignored ; priority remains 5.
Fixed, Non-Configurable ThreadGroup :
Every virtual thread belongs to a single JVM-managed group named VirtualThreads , and the Thread.ofVirtual() builder offers no way to choose a different group. ThreadGroup is effectively inert in Java 21: destroy() is terminally deprecated and simply does nothing, and setMaxPriority() never changes the priority of virtual threads. Treat thread groups as a legacy diagnostic label, not a control surface.
Thread.currentThread().isVirtual() :
Returns true if invoked on a virtual thread; false on platform threads. Thread vt = Thread.ofVirtual().unstarted(() -> {});
- System.out.println(vt.isDaemon()); // PRINTS: true
- System.out.println(vt.isVirtual()); // PRINTS: true
- System.out.println(vt.getPriority()); // PRINTS: 5
vt.setPriority(Thread.MAX_PRIORITY); // Silently ignored; priority remains 5
// vt.setDaemon(false); // COMPILE OK, but THROWS IllegalArgumentException at runtime!
5. Thread Pinning: Causes, Diagnostics, and Remediation
What is Thread Pinning?
When a virtual thread blocks while pinned , it cannot unmount from its carrier thread . Consequently, the underlying OS carrier thread remains blocked and unavailable to execute other virtual threads, severely degrading throughput.
The Two Pinning Triggers:
Executing inside a synchronized block or method : Entering an intrinsic monitor lock binds the virtual thread stack frame to the OS carrier thread's C-runtime stack.
Executing a Native Method or Foreign Function : JNI native calls or Foreign Function & Memory (FFM) API calls.
Remediation: Replace synchronized with ReentrantLock
Unlike intrinsic monitors ( synchronized ), java.util.concurrent.locks.ReentrantLock uses LockSupport.park() for blocking. LockSupport.park() supports virtual thread continuations, allowing virtual threads to unmount cleanly without pinning! // PINNING ANTI-PATTERN (Carrier thread is blocked!)
public class PinnedService {
public synchronized String fetchData() { // synchronized causes pinning!
try {
return httpCall(); // Blocking I/O while holding monitor = PINNED!
- } catch (Exception e) { return ""; }
- }
- }
- // REFACTORED BEST PRACTICE (Cleanly unmounts carrier thread!)
- public class SafeService {
- private final ReentrantLock lock = new ReentrantLock();
- public String fetchData() {
- lock.lock();
- try {
return httpCall(); // Virtual thread unmounts cleanly during I/O!
- } catch (Exception e) {
- return "";
- } finally {
- lock.unlock();
- }
- }
- }
- Diagnosing Pinned Threads via JVM Flag
You can detect pinning at runtime by launching the JVM with: -Djdk.tracePinnedThreads=full (prints full stack trace) or -Djdk.tracePinnedThreads=short (prints one-line summary).
- 6. Best Practices and Anti-Patterns
- Anti-Pattern 1: Pooling Virtual Threads
Do NOT Pool Virtual Threads : Never use Executors.newFixedThreadPool(100) with virtual threads. Pooling virtual threads adds memory overhead and defeats their design.
Best Practice : Create a new virtual thread per task using Executors.newVirtualThreadPerTaskExecutor() or Thread.ofVirtual().start() .
Anti-Pattern 2: CPU-Bound Tasks on Virtual Threads
Virtual threads provide zero throughput benefits for purely CPU-intensive computations (e.g., matrix math, video rendering, cryptographic hashing). Because CPU workloads cannot unmount, they monopolize carrier threads.
Best Practice : Use platform thread pools or ForkJoinPool.commonPool() for CPU-bound tasks.
Anti-Pattern 3: Heavy ThreadLocal Caching
Creating millions of virtual threads that each instantiate large cached objects in ThreadLocal can rapidly exhaust heap memory.
Best Practice : Avoid large ThreadLocal caches in virtual threads; prefer ephemeral task parameters or modern features like Scoped Values (JEP 446).
7. Java 21 Concurrency Preview Overview
Structured Concurrency (JEP 453 Preview) : Coordinates groups of related subtasks running in concurrent virtual threads as a single transaction using StructuredTaskScope ( ShutdownOnFailure for fail-fast or ShutdownOnSuccess for first-result racing).
Scoped Values (JEP 446 Preview) : Enables safe, immutable, and lightweight data sharing across threads without the memory overhead of ThreadLocal .