What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Virtual threads let Java applications run many concurrent, mostly waiting tasks without dedicating an operating-system thread to each one. Finalized in JDK 21, they keep the familiar java.lang.Thread model and can improve throughput when blocking I/O is a bottleneck—but they do not make code run faster or remove limits such as database connection capacity.

What virtual threads are

A virtual thread is a java.lang.Thread scheduled by the JDK rather than one tied for its entire lifetime to a single operating-system thread. The JDK runs virtual threads on a smaller set of platform threads, often called carrier threads. JEP 444 made virtual threads a permanent feature in JDK 21; they are not a preview feature in that release.

This preserves a familiar style of programming: a task can perform blocking operations in ordinary sequential code instead of being rewritten around a reactive API. Virtual threads are most useful when an application has many concurrent tasks that spend much of their time waiting, such as request handlers waiting for network or database I/O.

What happens when a virtual thread blocks

When a virtual thread blocks in supported blocking I/O, the JDK can suspend it and make its carrier available to run another virtual thread. The waiting task still exists, but it does not need to occupy that carrier while it waits. This is how an application can support many concurrent, waiting tasks with fewer operating-system threads than a one-platform-thread-per-task design would require.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

This behavior does not make every operation non-blocking, nor does it guarantee that every library or native call will release the carrier. Pinning is one important exception to understand when evaluating blocking work.

Are virtual threads faster than platform threads?

No—not in the sense of making an individual task execute faster. Oracle’s Virtual Threads documentation describes their purpose as scale and higher throughput, not speed or lower latency. If a task is CPU-bound, virtual threads do not reduce the amount of computation it must perform.

The practical differences depend on the workload and the system around it:

Consideration Virtual threads Platform threads
Many tasks waiting on blocking I/O Can support high concurrency by suspending eligible waiting tasks and reusing carriers. A blocked task occupies its platform thread while it waits.
CPU-bound tasks Do not make CPU instructions execute faster; throughput remains bounded by available compute. Also bounded by available compute; virtual threads offer no inherent speed advantage here.
Thread-per-task code Can preserve a thread-per-task programming style while using JDK-scheduled threads. Each task uses a platform thread, so very large numbers of concurrent threads can be costly.
Resource limits Do not increase capacity of databases, remote services, or other constrained dependencies. Likewise constrained by downstream capacity.

The qualitative comparison follows Oracle’s Java virtual-thread guidance and JEP 444; neither provides a universal percentage improvement. Results depend on queueing, downstream limits, allocation, scheduler behavior, and the application’s workload, so benchmark the actual service under representative conditions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What to expect when adopting them in production

Concurrency can increase without increasing dependency capacity

Virtual threads make it more practical to have a large number of tasks in flight, but that does not mean every dependency should receive an unlimited number of simultaneous requests. Keep explicit limits around scarce resources such as database connections. More virtual threads cannot create more connections or make an overloaded downstream service respond sooner.

Keep CPU-heavy work and waiting work distinct

Use virtual threads where tasks spend substantial time waiting, not as a way to accelerate long-running CPU-intensive work. In mixed workloads, assess the CPU-bound portion and its capacity separately from the I/O-waiting portion; adding concurrency to the latter cannot remove a compute bottleneck.

Validate the real service

Measure throughput, latency, resource use, queueing, and downstream behavior with the application’s actual workload. A change in thread model alone cannot predict the outcome: scheduler behavior, allocation, and dependency limits all affect results.

What virtual-thread pinning means

In Java 21, a virtual thread can be pinned to its carrier while executing a synchronized block or method, or while executing a native or foreign function. If a pinned virtual thread blocks for a long time, its carrier cannot be freed for other work during that interval. Pinning is not automatically a bug; frequent, long-lived pinning around blocking operations is the concern because it can reduce scalability.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

JEP 444 advises revising frequently used synchronized sections that guard potentially long I/O operations to use java.util.concurrent.locks.ReentrantLock instead. That is not a reason to replace every monitor: short in-memory critical sections and infrequent startup synchronization generally do not need to change.

Find pinning before changing synchronization

  • Use the JFR jdk.VirtualThreadPinned event to identify pinning. Oracle’s 2024 Java 21 documentation gives a default event threshold of 20 ms.
  • For thread dumps or tracing during investigation, Java 21 supports -Djdk.tracePinnedThreads=full and -Djdk.tracePinnedThreads=short.
  • Focus remediation on pinning that is both frequent and associated with long blocking operations; do not treat every event as proof that synchronization is harmful.

How to migrate an ExecutorService to virtual threads

For a thread-per-task executor, the direct JDK API is Executors.newVirtualThreadPerTaskExecutor(). A typical migration keeps task submission and shutdown explicit:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var first = executor.submit(() -> fetchFirst());
    var second = executor.submit(() -> fetchSecond());

    var firstResult = first.get();
    var secondResult = second.get();
}

This example assumes the enclosing method handles the checked exceptions from get(). The try-with-resources block closes the executor when the work is complete. The important design choice is that this executor creates a virtual thread per task; it is not a pool that limits task concurrency. Retain separate controls for resources whose capacity is scarce.

JDK Thread Builder APIs are another option when code needs to create threads directly. The migration is often straightforward for blocking thread-per-request code, but still review pinning, thread-local usage, downstream limits, and operational diagnostics rather than assuming that changing the executor alone resolves every scalability issue.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Observability, thread-local state, and related APIs

Inspect virtual-thread behavior

Java Flight Recorder can record virtual-thread start and end, pinning, and submission-failure events. JFR recordings can be examined with JDK Mission Control; jcmd can help inspect runtime behavior and work with recordings. Use these tools to understand what the application is doing under load rather than inferring health from the number of virtual threads alone.

Review per-thread state

JDK 21 guarantees thread-local support for virtual threads, which helps existing code keep working. But per-thread cached state can become expensive when an application creates very large numbers of virtual threads. Review uses of ThreadLocal that retain substantial state, and consider scoped values where their semantics fit. API availability and maturity depend on the JDK release.

Consider structured concurrency separately

Structured concurrency provides APIs for expressing related tasks, including fan-out relationships, with benefits for cancellation and observability. It complements virtual threads but is a separate API choice; its availability and maturity vary by JDK version. Check the documentation for the specific JDK you deploy before relying on it.

When virtual threads are a good fit

  • Your service handles many concurrent tasks that spend substantial time waiting on blocking I/O.
  • Your existing code uses a thread-per-request or thread-per-task model and you want to retain sequential, blocking-style code.
  • You can preserve explicit limits around constrained resources and can observe pinning and runtime behavior.

They are a weaker fit when the primary workload is long-running CPU computation, when downstream capacity is already the bottleneck, or when a service expects virtual threads by themselves to lower latency. The useful question is not whether virtual threads are universally better, but whether they let this workload handle its waiting tasks more effectively without overwhelming its dependencies.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.