Carrier Threads in Java Virtual Threads

Carrier threads are platform threads that the JVM uses to execute virtual threads. A virtual thread is mounted on a carrier while it runs and can be unmounted when it blocks, allowing the carrier to execute another virtual thread.

Virtual threads are lightweight Java threads managed by the JVM rather than being permanently tied to an operating-system thread. To execute Java code, however, a virtual thread must run on an underlying platform thread called its carrier thread.

The JVM scheduler mounts a runnable virtual thread onto a carrier, allowing its code to execute normally. When the virtual thread reaches an operation that can suspend it, such as supported blocking I/O, the JVM can unmount it and free the carrier to run another virtual thread.

This separation is central to virtual-thread scalability: thousands or millions of virtual threads can share a much smaller number of carrier threads. The application therefore does not need one permanently occupied platform thread for every concurrent task.

A carrier is an implementation detail of the virtual-thread runtime. Application code generally should not depend on which carrier executes a particular virtual thread, and a virtual thread may use different carriers during its lifetime.

Certain operations can prevent a virtual thread from being unmounted efficiently. In particular, long-running code inside some forms of `synchronized` regions or native/foreign code can pin a virtual thread to its carrier, reducing the scalability benefit.

Key points

Interview questions

What is a carrier thread in Java's virtual-thread model?

A carrier thread is a platform thread that runs a virtual thread's code. The JVM scheduler mounts a virtual thread onto a carrier when the virtual thread is runnable. When the virtual thread can suspend, it may be unmounted so the carrier can run other virtual threads. Application code normally does not manage individual carriers.

How do virtual threads achieve high concurrency if they still need platform threads to execute?

Virtual threads are multiplexed over a smaller set of carrier platform threads. A virtual thread can suspend during supported blocking operations, allowing its carrier to execute another runnable virtual thread. This means a large number of mostly waiting tasks does not require an equal number of platform threads. The result is high concurrency without requiring one permanently occupied platform thread per task.

Can a virtual thread move from one carrier thread to another?

Yes. A virtual thread is not permanently bound to one carrier. After it is unmounted, the JVM can later mount it on a carrier when it becomes runnable again. Therefore, application logic should not depend on a virtual thread retaining a particular carrier.

What is virtual-thread pinning and why does it matter?

Pinning occurs when a virtual thread cannot be efficiently unmounted from its carrier during an operation that prevents normal virtual-thread scheduling. A common case involves executing blocking work while holding a monitor in certain `synchronized` regions; native or foreign-function execution can also create constraints. If many virtual threads become pinned, carrier threads can remain occupied and concurrency can suffer. Engineers should therefore consider pinning when diagnosing unexpected scalability problems.

Should an application tune the carrier-thread pool like a normal executor pool?

Generally, application code should not treat carrier threads as ordinary worker threads because carrier scheduling is an implementation concern of the virtual-thread runtime. Application-level executors should represent application concurrency requirements rather than assumptions about carrier identity. If performance is poor, investigate CPU saturation, blocking behavior, synchronization, pinning, and resource limits instead of blindly increasing thread counts. The appropriate tuning depends on the workload and the Java runtime implementation.