Agency

Mastering Virtual Threads in Java 21: Building High-Concurrency Applications

Learn how Java 21 virtual threads simplify highly concurrent, I/O-heavy applications by removing platform thread scarcity without removing resource scarcity.

LAST UPDATED: March 08, 2026
7 min read
Mastering Virtual Threads in Java 21: Building High-Concurrency Applications

Java 21 made virtual threads a permanent part of the Java platform, changing how developers approach concurrency at scale. Instead of treating threads as scarce resources that must be carefully pooled, virtual threads make it practical to represent large numbers of concurrent tasks directly. This guide explains how virtual threads work, where they shine, where they do not, and how to use them effectively in production systems.

Why Virtual Threads Matter

For decades, Java applications commonly modeled concurrent work using platform threads.

The model was straightforward:

Request
   ↓
Platform Thread
   ↓
Application Work
   ↓
Response

But platform threads are relatively expensive operating-system resources.

Creating thousands—or potentially millions—of them is not generally practical.

As a result, applications traditionally introduced thread pools:

Requests
   ↓
Thread Pool
   ├── Thread 1
   ├── Thread 2
   ├── Thread 3
   └── Thread N

This works well until the application has far more concurrent tasks than available threads.

Virtual threads change the economics.

Instead of making application concurrency depend directly on a large number of operating-system threads, Java can represent large numbers of lightweight concurrent tasks.

The model becomes:

Thousands of Tasks
        ↓
Virtual Threads
        ↓
Small Number of Carrier Threads
        ↓
CPU

That is the fundamental shift.

Platform Threads vs. Virtual Threads

A platform thread is closely associated with an operating-system thread.

A virtual thread is managed by the Java runtime and can be mounted onto a platform thread when it needs to execute.

Conceptually:

Platform Threads

Java Thread ───────── OS Thread
Java Thread ───────── OS Thread
Java Thread ───────── OS Thread

With virtual threads:

Virtual Threads
   │
   ├── Task A
   ├── Task B
   ├── Task C
   ├── Task D
   └── Task ...
          ↓
    Carrier Threads
          ↓
       OS Threads

When a virtual thread blocks on supported operations such as many forms of I/O, the runtime can suspend it and allow the underlying carrier thread to execute other work.

This is what makes large-scale concurrency practical without requiring one operating-system thread for every concurrent task.

How Virtual Threads Actually Work

Virtual threads are designed around a simple idea:

A concurrent task does not need to permanently occupy an operating-system thread while it is waiting.

Consider an API request:

Request
  ↓
Read Database
  ↓
Wait...
  ↓
Process Result
  ↓
Send Response

During the database wait, the application does not need to spend valuable CPU time executing that request.

With virtual threads, the runtime can suspend the waiting virtual thread and use the carrier thread for other runnable work.

Conceptually:

Virtual Thread A
   ↓
Database Wait
   ↓
Suspended

Carrier Thread
   ↓
Virtual Thread B
   ↓
Execute

Carrier Thread
   ↓
Virtual Thread C
   ↓
Execute

This allows applications to support much higher concurrency for I/O-heavy workloads.

Importantly, virtual threads do not make the CPU faster.

They make it cheaper to have many concurrent tasks waiting for external resources.

Creating and Running Virtual Threads

Java 21 provides straightforward APIs for creating virtual threads.

For example:

Thread.startVirtualThread(() -> {
    processRequest();
});

You can also use an executor designed for one virtual thread per submitted task:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> fetchCustomer());
    executor.submit(() -> fetchOrders());
}

The important conceptual difference is:

You are creating a virtual thread per task, rather than maintaining a fixed pool of expensive platform threads.

This makes the code model closer to the actual business operation.

Why Virtual Threads Change Server Architecture

Traditional server applications often optimize around thread scarcity.

For example:

1000 Requests
      ↓
50 Worker Threads
      ↓
Queue
      ↓
Processing

The queue exists partly because threads are expensive.

Virtual threads allow a different model:

1000 Requests
      ↓
1000 Virtual Threads
      ↓
Concurrent I/O
      ↓
Responses

This can simplify application architecture for workloads dominated by blocking operations.

For example, a request might call:

API
 ↓
Database
 ↓
Payment Service
 ↓
Inventory Service
 ↓
Response

Instead of designing every operation around asynchronous callback chains, developers can often write straightforward sequential-looking code while still supporting high concurrency.

That can improve readability significantly.

Virtual Threads and Blocking I/O

This is where virtual threads are particularly valuable.

Consider:

var customer = customerRepository.findById(id);
var orders = orderService.getOrders(customer.id());
var recommendations = recommendationService.get(customer.id());

Each operation may involve waiting.

With platform threads, every blocked request occupies an operating-system thread.

With virtual threads, the runtime can often suspend the waiting virtual thread and reuse the carrier thread for another task.

This is especially useful for applications involving:

HTTP calls

Database access

File operations

Message brokers

Remote services

The important word is I/O.

Virtual threads are not a magic performance boost for CPU-heavy computation.

Structured Concurrency and Task Design

Virtual threads work especially well with a programming model where concurrent tasks have clear ownership and lifetimes.

For example, imagine a request that needs three independent pieces of information:

             Request
                │
       ┌────────┼────────┐
       ▼        ▼        ▼
    Profile   Orders   Rewards
       │        │        │
       └────────┼────────┘
                ▼
             Response

These operations can execute concurrently.

The broader goal is to make concurrency easier to reason about:

Tasks should have clear lifetimes, ownership, and error handling.

Java's structured concurrency work is designed around this direction, although developers should distinguish Java 21's finalized virtual threads from APIs that were still evolving through preview releases in that era.

The architectural principle remains valuable:

Start related tasks together.

Wait for related tasks together.

Handle failures together.

Cancel work that no longer matters.

Thread Pools: What Changes?

One of the biggest conceptual adjustments with virtual threads is understanding that traditional thread pools are no longer automatically the answer to every concurrency problem.

Developers often write:

Executors.newFixedThreadPool(100);

because platform threads are expensive.

With virtual threads, you can often write:

Executors.newVirtualThreadPerTaskExecutor();

and let the runtime manage the underlying carrier threads.

But this does not mean:

"Remove every limit."

Virtual threads are cheap.

Your external resources are not.

For example:

100,000 Virtual Threads
        ↓
Database
        ↓
100 Connections

The database still has only 100 connections.

That leads to one of the most important virtual-thread lessons:

Virtual threads remove thread scarcity. They do not remove resource scarcity.

Database Connections Are Still a Bottleneck

Imagine an application receiving 20,000 concurrent requests.

Each request starts a virtual thread.

That is fine.

But if every request immediately attempts to obtain a database connection:

20,000 Virtual Threads
        ↓
Database Connection Pool
        ↓
100 Connections

the database becomes the bottleneck.

You still need appropriate limits around:

Database connections

External API concurrency

File descriptors

Memory

CPU

Rate limits

Message broker capacity

This is why virtual-thread adoption should be paired with resource-aware architecture.

The goal is:

High Concurrency
      ↓
Controlled Resource Usage
      ↓
Stable System

Common Virtual Thread Mistakes

Treating Virtual Threads Like Faster Platform Threads

They are primarily a scalability and concurrency model—not a faster CPU execution mechanism.

Creating Huge Unbounded Workloads

Virtual threads are lightweight, but millions of active tasks can still consume significant memory and overwhelm downstream services.

Ignoring External Resource Limits

Database pools, APIs, and message brokers still have capacity limits.

Using Virtual Threads for CPU-Bound Work

If the application is primarily performing expensive computation, more virtual threads will not magically improve CPU throughput.

Use appropriate parallelism based on available processors and workload characteristics.

Blocking on Problematic Constructs

Some operations can prevent a virtual thread from yielding efficiently, depending on the underlying implementation and runtime behavior.

Profile real workloads instead of assuming every blocking call behaves identically.

Keeping Old Thread-Pool Assumptions Everywhere

Virtual threads may allow simpler concurrency designs.

Do not automatically reproduce platform-thread pooling patterns without considering why the pool existed in the first place.

Ignoring Thread-Local Usage

Virtual threads can exist in very large numbers.

Code that creates large or expensive `ThreadLocal` state per thread can therefore become surprisingly costly.

Review thread-local usage carefully.

Observability and Production Considerations

A system with thousands of concurrent tasks needs strong observability.

Monitor:

Request latency

Throughput

Virtual-thread counts

CPU utilization

Memory usage

Database pool utilization

External API latency

Queue depth

Error rates

A useful model is:

Requests
   ↓
Virtual Threads
   ↓
Application
   ↓
External Resources
   ↓
Metrics + Traces

Do not simply monitor:

"How many threads are running?"

Instead, ask:

Where are tasks waiting?

Which resource is limiting throughput?

Are virtual threads improving concurrency without overwhelming downstream systems?

Distributed tracing is particularly useful when one request performs multiple remote operations.

A Practical Migration Strategy

Step 1: Identify I/O-Heavy Workloads

Look for applications dominated by:

HTTP calls

Database queries

File I/O

Remote services

These are strong candidates.

Step 2: Upgrade to a Suitable Java Version

Java 21 is an LTS release and provides finalized virtual threads.

Step 3: Start With a Focused Service

Do not immediately rewrite the entire platform.

Choose one service with a measurable concurrency bottleneck.

Step 4: Replace Unnecessary Platform-Thread Pools

Evaluate whether a virtual-thread-per-task executor is appropriate.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // Submit independent tasks
}

Step 5: Review Resource Limits

Measure:

Database pool size

HTTP connection pools

External API limits

Memory

CPU

Virtual-thread concurrency should be designed around these constraints.

Step 6: Test Under Realistic Load

Compare:

Before
Platform Threads
      ↓
After
Virtual Threads

Measure:

Latency

Throughput

Memory

CPU

Resource utilization

Step 7: Inspect Blocking Behavior

Profile the application to identify where virtual threads spend their time.

Step 8: Roll Out Gradually

Deploy to a controlled environment first.

Observe production behavior before expanding adoption.

When Not to Use Virtual Threads

Virtual threads are powerful, but they are not the right tool for every workload.

Be cautious when the workload is primarily:

CPU-bound

GPU-bound

Memory-bound

Highly dependent on thread-local state

Already well optimized around asynchronous I/O

For CPU-heavy workloads, the limiting resource is usually processor capacity—not the number of threads available.

For example:

CPU-Bound Work
     ↓
Available CPU Cores
     ↓
Parallelism Limit

Creating tens of thousands of virtual threads does not create tens of thousands of CPU cores.

Virtual threads shine when applications have many concurrent tasks that spend significant time waiting.

The Future of Java Concurrency

Virtual threads represent a broader change in how Java developers think about concurrency.

The traditional mindset was:

Threads are expensive. Minimize them.

The newer model becomes:

Tasks are cheap. Model concurrency around the work itself.

That can lead to simpler application code:

Request
 ↓
Call Service
 ↓
Wait
 ↓
Call Database
 ↓
Wait
 ↓
Return

while still supporting large numbers of concurrent requests.

Combined with modern Java features, developers can build applications around clearer concurrency boundaries rather than complicated thread-management machinery.

The result is not merely a performance feature.

It is a shift toward making highly concurrent software easier to express.

Making the Call

Engineering teams evaluating virtual threads should ask:

Is our workload I/O-heavy?

Are platform-thread pools limiting concurrency?

Where do requests spend most of their time?

What external resources limit throughput?

Can our database handle increased concurrency?

Are our HTTP clients configured appropriately?

Do we have enough observability to measure the change?

Are we accidentally using virtual threads to solve a CPU problem?

Most importantly:

Are we optimizing thread count, or are we optimizing the actual system bottleneck?

That distinction is critical.

Virtual threads can remove one constraint while exposing another.

Final Takeaway

Java 21's virtual threads make high concurrency dramatically easier to express.

The architecture changes from:

Requests
   ↓
Limited Platform Threads
   ↓
Queue
   ↓
Work

toward:

Requests
   ↓
Virtual Threads
   ↓
Concurrent I/O
   ↓
Controlled Resources
   ↓
Response

They are especially powerful for I/O-heavy applications where many tasks spend time waiting.

But virtual threads are not unlimited capacity.

They do not make databases faster.

They do not increase CPU cores.

They do not remove API rate limits.

They do not eliminate the need for backpressure.

Instead, they give developers a simpler and more scalable way to represent concurrent work.

The biggest advantage of virtual threads is not that Java can create more threads. It is that developers can model concurrency around tasks instead of treating operating-system threads as scarce application resources.

Start with I/O-heavy services.

Measure real bottlenecks.

Replace unnecessary platform-thread pooling.

Keep limits around databases and external systems.

Monitor memory and latency.

Test under realistic load.

And adopt virtual threads because they simplify the right workload—not because "more threads" sounds faster.

Java 21 makes concurrency lighter. Your architecture still needs to make it disciplined.

Frequently Asked Questions

No, virtual threads do not make the CPU faster or execute code more quickly. They make it much cheaper to have many concurrent tasks waiting for external resources (like database queries or API calls). They remove thread scarcity, allowing the application to scale I/O concurrency without being bottlenecked by expensive operating-system threads.
While you no longer need traditional thread pools to manage scarcity of platform threads, you may still need concurrency limits to protect downstream resources like databases, APIs, or memory. You can often replace platform-thread pools with a virtual-thread-per-task executor, but you must ensure external resources can handle the resulting concurrency.
When a virtual thread blocks on a supported I/O operation (like a network call or database query), the Java runtime can suspend the virtual thread and reuse the underlying "carrier" platform thread to execute other virtual threads. This is what enables thousands of virtual threads to run efficiently on a small number of platform threads.

Need a product built?

We build custom software, mobile apps, and web platforms for startups and enterprises.

Alejandro D.
Vatsalya R.Backend Developer
Gustavo A.
Ganeshan S.Sr. Software Engineer
Fiorella G.
Uptal JoshiSr. Data Scientist

Their team became an extension of ours — within months they'd rebuilt our entire product experience from the ground up.

BitForge
Sr. ArchitectBitForge
Read Case Study