Fundamentals 28 min read

Why Java’s New Virtual Threads Could Transform High‑Scale Applications

Java 19 introduces preview virtual threads—a lightweight alternative to platform threads that store stack frames on the heap, dramatically reduce memory overhead, and eliminate the thread‑count scalability bottleneck, enabling a simple “one task per thread” model for IO‑bound workloads without sacrificing existing thread semantics.

Programmer DD
Programmer DD
Programmer DD
Why Java’s New Virtual Threads Could Transform High‑Scale Applications

Java 19 brings the first preview of virtual threads, a major outcome of the OpenJDK Loom project. Virtual threads fundamentally change how the Java runtime interacts with the operating system, removing scalability barriers while requiring little new learning for developers.

Threads

Threads are the foundation of Java. The main method runs on the "main" thread created by the launcher. Method calls execute on the same thread, with call frames stored on the thread stack, enabling stack traces for debugging. Threads provide sequential control flow, local variables, exception handling, debugging, profiling, and are the basic scheduling unit; when a thread blocks on I/O, locks, or storage, it is descheduled so other threads can run.

Despite their usefulness, threads have a reputation for difficulty because most developers use them to manage shared mutable state, which requires understanding subtle concepts like memory visibility and many programming principles.

Nevertheless, for the vast majority of time threads work silently and reliably, providing stack‑based exception handling, observability, remote debugging, and a perception of sequential execution.

Platform Threads

Java provides a complete, portable abstraction for threads, including coordination mechanisms and a memory model that gives predictable semantics across many platforms.

Most JVMs implement Java threads as simple wrappers around operating‑system threads, called platform threads. While OS support for threads is now robust, platform threads are heavyweight: creating them is costly in time and memory, and each thread reserves a large stack (often megabytes), limiting the number of concurrent threads an application can create.

Because each thread carries a sizable stack, configuring stack size is risky; too large wastes memory, too small risks StackOverflowError. Consequently, the common "one task per thread" model works well for moderate workloads (e.g., 1,000 concurrent requests) but breaks down for larger scales.

To handle massive concurrency, developers traditionally resort to limiting code patterns, adding hardware, or switching to asynchronous/reactive programming, which sacrifices many of the conveniences of the thread‑based model.

Virtual Threads

Virtual threads are another implementation of java.lang.Thread. Their stack frames reside on the Java heap instead of a contiguous OS‑allocated memory block, starting with only a few hundred bytes and growing or shrinking as needed.

Only platform threads are known to the OS. To run code on a virtual thread, the JVM mounts it onto a carrier (platform) thread. When a virtual thread blocks on I/O, locks, or other resources, it is unmounted: its stack frames are copied back to the heap, and the carrier thread is released for other work.

This mounting/unmounting is invisible to Java code; the current thread always appears as the virtual thread, and ThreadLocal values of the carrier are not visible to the virtual thread.

The name "virtual" reflects similarity to virtual memory: many cheap virtual threads share a few scarce carrier threads, with inactive virtual stacks paged to the heap.

Virtual threads expose almost the same API as platform threads. New factory methods such as Thread.ofVirtual() or Executors.newVirtualThreadPerTaskExecutor() create virtual threads, but after creation they behave like ordinary Thread objects.

Example: concurrently fetching two URLs using virtual threads.

void handle(Request request, Response response) {
    var url1 = ...
    var url2 = ...
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        var future1 = executor.submit(() -> fetchURL(url1));
        var future2 = executor.submit(() -> fetchURL(url2));
        response.send(future1.get() + future2.get());
    } catch (ExecutionException | InterruptedException e) {
        response.fail(e);
    }
}

String fetchURL(URL url) throws IOException {
    try (var in = url.openStream()) {
        return new String(in.readAllBytes(), StandardCharsets.UTF_8);
    }
}

Even though creating a thread for a tiny task may seem wasteful, virtual threads make this perfectly acceptable.

Is This Just a “Green Thread”?

Early JVMs used "green" threads managed by the JVM rather than the OS, but those still allocated large stacks and were a product of single‑core systems. Virtual threads differ: they are lightweight, integrate with existing thread semantics, and resemble user‑mode threads in languages like Go or Erlang.

All About Scalability

Virtual threads are not faster than platform threads for CPU‑bound work, but they are cheap enough to create millions of idle threads. Most server workloads spend the majority of time waiting on I/O, so having a thread per task removes the thread‑count bottleneck and improves hardware utilization.

For CPU‑intensive workloads, tools like the fork‑join framework and parallel streams remain appropriate; virtual threads mainly benefit I/O‑bound workloads.

Little’s Law

A stable system’s scalability follows Little’s Law: throughput T = N / d, where d is request latency and N is the number of concurrent tasks. When thread count limits N, throughput suffers. Virtual threads increase N without changing the programming model.

T = N / d

Virtual Threads in Practice

Virtual threads complement, not replace, platform threads. For example, creating 100,000 virtual threads that each sleep for one second:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
} // executor.close() called automatically

On a typical desktop, this program starts in about 1.6 seconds (cold) and 1.1 seconds after warm‑up. Using a fixed pool of 1,000 platform threads would take roughly 100 seconds, as predicted by Little’s Law.

Things to Unlearn

Since virtual threads behave like regular threads, there is little new API to learn, but developers must unlearn certain old practices.

Avoid Using Thread Pools

Creating a thread pool for virtual threads is an anti‑pattern. The low cost of virtual thread creation makes pooling unnecessary and can reintroduce problems like ThreadLocal leakage.

If you need to limit concurrency for external resources (e.g., database connections), use a Semaphore instead.

Abusing ThreadLocal

ThreadLocal caches that are reasonable for hundreds of threads become problematic with millions of virtual threads, leading to excessive memory usage and reduced reuse. In such cases, explicit pooling or alternative designs are preferable.

What About Reactive Programming?

Reactive frameworks achieve high scalability by decoupling I/O from threads, requiring callbacks and async composition, which sacrifices language‑level constructs like loops, try‑catch, and straightforward debugging. Virtual threads provide similar scalability without abandoning these familiar constructs.

API and Platform Changes

Virtual threads are a preview feature requiring the --enable-preview flag. New factory methods ( Thread.ofVirtual, Thread.ofPlatform), a Thread.Builder, and Thread.startVirtualThread have been added. Virtual threads are always daemon, have fixed priority, and do not support legacy mechanisms like ThreadGroup or stop.

Idle virtual threads can be garbage‑collected, and their stacks are reclaimed when not in use.

JDK Preparations

New socket implementations (JEP 353, JEP 373) to make blocking I/O interruptible for virtual threads.

Virtual‑thread awareness : most blocking points now detect virtual threads and unload them.

ThreadLocal revisions to accommodate new usage patterns.

Lock revisions : internal synchronized can pin a virtual thread, so ReentrantLock is preferred.

Improved thread dumps with better filtering and grouping of virtual threads.

Related Work

Beyond virtual threads, Loom includes a structured concurrency framework for coordinating groups of virtual threads and an "extent local" variable concept that offers better performance than traditional ThreadLocal in virtual‑thread contexts.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performancevirtual-threadsjava19
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.