Java Virtual Threads

Java Programming Tutorials

Miscellaneous

Java virtual threads are lightweight threads managed by the Java runtime. They became a permanent feature in Java 21 and let thread-per-request applications handle many concurrent tasks without assigning one operating-system thread to every task.

Virtual threads improve throughput when tasks spend much of their time waiting for network, database, or file I/O. They do not make CPU-intensive calculations run faster, and they do not create more processor cores.

Use virtual threads to keep straightforward blocking code while supporting high concurrency. Measure the complete workload before replacing an existing concurrency design.

Platform Threads and Virtual Threads

Feature Platform Thread Virtual Thread
Scheduling Primarily operating system Java runtime on carrier threads
Typical resource cost Relatively high Low
Suitable count Usually limited Potentially very large
Best fit CPU work and existing pools Many blocking I/O tasks

A virtual thread runs on a platform thread called its carrier. When supported blocking operations wait, the runtime can unmount the virtual thread and use the carrier for other work. Your code still sees the current virtual thread, not its carrier.

Start a Virtual Thread

Use Thread.startVirtualThread() for a direct one-task example. The method creates and starts the thread.

Example:

// Start one virtual thread and wait for its result
Thread worker = Thread.startVirtualThread(() -> {
    System.out.println("Running on: " + Thread.currentThread());
});

worker.join();

You can use the builder API when you need names or an unstarted thread.

// Name virtual threads to improve logs and diagnostics
Thread.Builder builder = Thread.ofVirtual().name("invoice-", 0);
Thread invoice = builder.start(() -> loadInvoice(1042));
invoice.join();

Use a Virtual-Thread-Per-Task Executor

Executors.newVirtualThreadPerTaskExecutor() creates a new virtual thread for every submitted task. The executor is unbounded, so it does not act as a concurrency limiter.

import java.util.concurrent.*;

public class InvoiceLoader {
    public static void main(String[] args) throws Exception {
        // The executor closes after submitted tasks finish
        try (ExecutorService executor =
                 Executors.newVirtualThreadPerTaskExecutor()) {

            Future<String> delhi =
                executor.submit(() -> fetchInvoice("Delhi"));
            Future<String> london =
                executor.submit(() -> fetchInvoice("London"));

            System.out.println(delhi.get());
            System.out.println(london.get());
        }
    }

    static String fetchInvoice(String office) throws InterruptedException {
        // Simulate blocking I/O rather than CPU-heavy work
        Thread.sleep(200);
        return office + " invoice loaded";
    }
}

Do Not Pool Virtual Threads

Traditional pools reuse expensive platform threads and limit concurrency. Virtual threads are intended to be cheap and short-lived, so create one per task. If a downstream service can handle only a fixed number of requests, limit access to that service with a semaphore or another explicit mechanism.

// Protect a database that allows only 20 concurrent operations
private static final Semaphore DB_LIMIT = new Semaphore(20);

static Order loadOrder(long id) throws Exception {
    DB_LIMIT.acquire();
    try {
        // This blocking call can run inside a virtual thread
        return orderRepository.find(id);
    } finally {
        DB_LIMIT.release();
    }
}

Cancellation and Interruption

Virtual threads use the existing Java interruption model. Cancelling a submitted Future with true interrupts the thread. Blocking APIs may throw InterruptedException; restore the interrupt status when you cannot propagate it.

try {
    Thread.sleep(1_000);
} catch (InterruptedException error) {
    // Preserve cancellation information for calling code
    Thread.currentThread().interrupt();
    return;
}

Understand Pinning and Java Versions

In Java 21 through 23, a virtual thread can remain attached to its carrier when it blocks inside a synchronized method or block, reducing scalability if pinning is frequent and long. Java 24 delivered JEP 491, allowing virtual threads to unmount in nearly all synchronized cases.

Native or foreign-function calls can still pin a virtual thread. If you support older Java releases, keep long blocking operations outside synchronized regions or use suitable java.util.concurrent locks. On Java 21, -Djdk.tracePinnedThreads=full can help diagnose blocking pinning; Java 24 removed the need for that diagnostic for synchronized code.

Use ThreadLocal Carefully

Virtual threads support ThreadLocal, but an application may create a huge number of them. Large per-thread values can therefore consume substantial memory. Avoid using thread locals as hidden caches, and remove values when their lifetime ends.

Observability and Debugging

Give important virtual threads meaningful names, inspect thread dumps, and record Java Flight Recorder events when diagnosing scale problems. Do not infer that a large thread count is itself an error; examine blocked operations, memory use, downstream limits, latency, and carrier utilization.

When to Use Virtual Threads

  • Request handlers that call databases or remote services.
  • Applications with many concurrent blocking I/O operations.
  • Code that benefits from a clear sequential style.
  • Migration from large platform-thread pools after measurement.

When Virtual Threads Do Not Help

  • Long-running CPU-bound calculations.
  • Workloads already constrained by a database or API limit.
  • Very short tasks where concurrency overhead is not the bottleneck.
  • Code that relies on large per-thread caches.

Best Practices

  • Use Java 21 or later and document the deployed JDK version.
  • Create a virtual thread per task instead of pooling them.
  • Limit scarce external resources explicitly.
  • Handle interruption and executor shutdown correctly.
  • Load-test realistic I/O, memory, and downstream behavior.
  • Review pinning requirements for the exact JDK version.

Conclusion

Java virtual threads make the thread-per-task model practical for highly concurrent I/O workloads. Start them directly or through the per-task executor, preserve normal cancellation rules, limit scarce services separately, and understand version-specific pinning behavior. Used for the right workload, they improve scalability while keeping code readable.



Found This Page Useful? Share It!
Get the Latest Tutorials and Updates
Join us on Telegram