From Platform Threads to Virtual Threads: A New Era of Java Concurrency

From Platform Threads to Virtual Threads: A New Era of Java Concurrency

Priyanshu JethanandaniNovember 11, 2025
Share this article From Platform Threads to Virtual Threads: A New Era of Java Concurrency From Platform Threads to Virtual Threads: A New Era of Java Concurrency From Platform Threads to Virtual Threads: A New Era of Java Concurrency

Table of Contents

    In Java, threads are the foundation of concurrency. They allow multiple tasks to run concurrently, making applications more responsive. making applications more responsive. For year, Java have relied on platform thread, where every Java thread be mapped to an operating system thread. nonetheless, this work okay for CPU-intensive workload, but it become a trouble when an application computer program needs to handle tens or hundreds of thousands of simultaneous I/O mathematical operation — for example, World Wide Web request, database calls, or message processing.

    To avoid creating thousands of OS threads (which are expensive in terms of memory and context-switching), developers have been forced to use thread pools, asynchronous code, reactive frameworks, and complicated callback-based designs. These solutions work, but they make the code harder to read, debug, and maintain.

    To solve this, Java introduced virtual threads (Project Loom) — a lightweight, user-mode implementation of threads that behave like normal java.lang. Hence, Thread objects, but are not tied 1:1 to OS threads. Virtual thread is cheap to make, schedule, and block. When a virtual thread performs a blocking operation, the JVM simply parks it and frees the underlying carrier thread, which can immediately run another virtual thread. nonetheless, The final result: millions of concurrent tasks without the complexity of async code or reactive frameworks.

    Creating a Virtual Thread

    Creating a virtual thread looks almost the same as creating a regular thread — the difference is how it’s started:

    
    Thread vt = Thread.startVirtualThread(() -> {    
    System.out.println("Running inside a virtual thread: " + Thread.currentThread());
    });
    

    So how do virtual threads really compare to platform threads? Let’s put them side by side.

    At first glance, a virtual thread looks just like any other Java thread — you can create it, start it, join it, and use the same APIs (Thread, Executor Service, CompletableFuture, etc.). Furthermore, the real difference lies under the hood — in how the JVM schedules and manages them.

    1. Thread-to-OS Mapping

    • Platform threads: Each Java thread is directly mapped to an operating system (OS) thread. Additionally, this means every Java thread consumes native memory (normally 1–2 MB stack) and system resource. Nonetheless, creating thousands of them can quickly exhaust system limits.
    • Virtual threads: They are managed entirely by the JVM, not the OS. thus, Multiple virtual threads share a small pocket billiards of common carrier thread (real OS thread). When a virtual thread blocks (e.g., waiting for I/O), it’ s parked by the JVM and the carrier thread is release to run for something else.
    1. Scheduling

    • Platform threads: Scheduling is handled by the operating system’s thread scheduler.
    • Virtual threads: Scheduling happens in the JVM, in user space, using a work-stealing scheduler. This gives the runtime far more control, and context switches happen much faster since they don’t involve the OS kernel.
    1. Blocking Behavior

    • Platform threads: A blocking call (like waiting on I/O or a database) keeps the OS thread busy and unavailable for other work.
    • Virtual threads: When they block on a Loom-aware API (like most modern I/O in Java 21+), the JVM automatically suspends the virtual thread and frees the carrier thread. No busy waiting, no wasted resources.
    1. Programming Model

    • Platform threads: To scale efficiently, you often have to switch to asynchronous or reactive programming models (CompletableFuture, Reactor, etc.), which adds complexity.
    • Virtual threads: You can use simple, blocking, imperative code and still achieve massive scalability. That means less boilerplate, fewer callbacks, and easier debugging.

    Creating Virtual Threads in Practice

    There are a couple of ways to create virtual threads in Java. Both are simple, but they serve slightly different use cases — direct thread creation and task execution through an executor.

    Using Thread.ofVirtual()

    The Thread.ofVirtual() factory provides a fluent API to create and start a virtual thread manually.

    This is ideal when you just want to spin up one or a few lightweight threads for demonstration or fine-grained control.

    
    Thread vThread = Thread.ofVirtual(). start (() -> {    
    System.out.println("Running in: " + Thread.currentThread());}
    ); 
    // Wait for it to finishvThread.join();
    

    Thread.ofVirtual() returns a Thread.Builder, which you can utilize to configure and start a virtual thread.

    When start () is called, the JVM schedules the virtual thread on a carrier thread from its internal pool.

    join () works exactly like it does with a normal thread — same API, no learning curve.

    Using Executors.newVirtualThreadPerTaskExecutor()

    For most real applications, you’ll want to manage tasks instead of threads directly.

    This is where the virtual thread executor shines — it creates a new virtual thread for each submitted task and automatically cleans it up when the task completes.

    
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {     
    // Submit multiple tasks    
    for (int i = 0; i < 5; i++) { 
    int taskId = i;
    executor.submit(() -> {
    System.out.println("Task " + taskId + " running in: " + Thread.currentThread());
    Thread.sleep(1000); // Simulate work  
    return taskId;
    }); 
    }    // Executor closes automatically at the end of the try-with-resources block}
    
    

    Each task runs in its own virtual thread, so you don’t need to worry about thread pools, tuning, or queue management.

    Since virtual threads are cheap, this model scales effortlessly — millions of short-lived tasks are feasible.

    You can still use all standard concurrency utilities (Future, CompletableFuture, etc.) on top of it.

    When to Use Which

    • Use ofVirtual() when you need direct, explicit control over thread lifecycle (e.g., for small demos or low-level libraries).
    • Use newVirtualThreadPerTaskExecutor() for real-world scenarios — servers, services, or any concurrent workloads that spawn large numbers of tasks.

    Conceptually, virtual threads sound outstanding — but allow’ s see what that means in genuine number.

    A classic pain point with platform threads is their high memory and scheduling overhead. Even a few one thousand of them can force a system of rules to its limits. moreover, Consequently, Virtual thread, on the other, be designed to handle millions of concurrent tasks with ease.

    Furthermore, Let’s test that difference with a simple benchmark.

    Test Goal

    Create and run 1 million lightweight tasks, each doing a trivial job (e.g., sleeping for a short time).

    We’ll measure:

    How long it takes to start and complete all tasks.

    Whether the system stays responsive or starts choking.

    Platform Thread Version

    
    public class PlatformThreadBenchmark {    
    public static void main(String[] args) throws InterruptedException {        
    int taskCount = 100_000; // reduce to 100k for platform threads        
    List<Thread> threads = new ArrayList<>();         
    long start = System.currentTimeMillis();         
    for (int i = 0; i < taskCount; i++) {            
    Thread t = new Thread(() -> {                
    try {                    
    Thread.sleep(1000);                
    } 
    catch (InterruptedException ignored) {}            
    });            
    threads.add(t);            
    t.start();        
    }         
    for (Thread t : threads) {            
    t.join();        
    }         
    long end = System.currentTimeMillis();        
    System.out.println("Platform threads completed in: " + (end - start) + " ms");    
    }}
    

    Running this with 1 million threads will likely crash or freeze your system due to memory exhaustion.

    Each platform thread consumes ~1MB of stack space by default, so 1M threads = ~1TB of memory.

    Virtual Thread Version

    
    public class VirtualThreadBenchmark {
    public static void main(String[] args) throws Exception {
    int taskCount = 100_000;
    
    long start = System.currentTimeMillis();
    
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < taskCount; i++) {
    executor.submit(() -> {
    Thread.sleep(1000);
    return null;
    });
    }
    } // auto-closes and waits for all tasks
    
    long end = System.currentTimeMillis();
    System.out.println("100k Virtual threads completed in: " + (end - start) + " ms");
    }
    }
    

    As the benchmark shows, 100K platform thread took just about 25 seconds to complete, while 100K virtual threads finished in just over 6 seconds — nearly four times quicker. Furthermore, to a greater extent importantly, the virtual-thread edition run with a fraction of the computer memory footprint and zero configuration change. This demonstrates exactly why virtual threads are a game-changer for I/O-heavy concurrency in Java.

    Conclusion

    Virtual threads don’t magically make code faster — they make concurrency radically simpler and more scalable. You can finally write straightforward, blocking-style code and still handle hundreds of thousands (or even millions) of concurrent tasks without the overhead of complex async frameworks or massive thread pools.

    • Platform threads are OS-managed and expensive.
    • Virtual threads are JVM-managed and lightweight.
    • Your code stays the same — only the runtime changes.

    Try running the benchmark yourself with different workloads.

    From Platform Threads to Virtual Threads: A New Era of Java Concurrency Priyanshu Jethanandani

    Software Engineer at NextGensoft

    Leave a Reply

    Your email address will not be published. Required fields are marked *


      Talk to an Expert

      100% confidential and secure