Java Virtual Threads

Java Virtual Threads: Unveiling Scalable Concurrency

Discover the power of virtual threads in Java! Learn their benefits, differences from platform threads, and how they optimize concurrency in high-throughput applications.

What Is Process?

A process is like a container for a program running on a computer. It includes everything that program needs to run, like its memory, data, and resources. When you open software, it starts as a process. A process can have one or more threads.

What Are Threads?

Threads, on the other hand, are smaller units within a process. They’re like workers inside that process, each doing a specific job. Just as different workers in a factory handle various tasks to build something, threads handle different jobs simultaneously within a program. They share resources within the same process and work together to make the program run efficiently.

A thread is the smallest unit of processing that can be scheduled by the operating system. It runs concurrently with and largely independently of other threads. In Java, a thread is an instance of the java.lang.Thread class.

What Are Platform Threads?

A platform thread is a thread that is implemented as a thin wrapper around an operating system (OS) thread. Platform threads are essentially a bridge between a thread in a Java program and the actual threads managed by the operating system. Each platform thread in Java corresponds to a thread managed by the underlying operating system. These threads in Java allow your code to run on specific threads provided by the operating system.

Since each platform thread in Java is tied to an actual operating system thread. This connection limits the number of available platform threads to the available operating system threads. Platform threads come with their own allocated resources, such as a substantial thread stack managed by the operating system. They enable Java code to operate within the boundaries and capabilities of the underlying operating system threads. These limitations sparked the innovation of virtual threads



The Emergence of Java Virtual Threads

Virtual threads are one of the most important innovations in Java for a long time. They were developed in Project Loom, which is an effort to simplify concurrent programming in Java by providing a new concurrency model based on lightweight threads. Virtual threads(JEP 444) were first introduced as a preview feature in JDK 19, delivered to the second preview round in JDK 20, and are now fully released as a part of JDK 21.

What Are Virtual Threads?

A virtual thread is a thread that is not tied to a specific OS thread. A virtual thread still runs code on an OS thread, but when the code calls a blocking I/O operation, the Java runtime suspends the virtual thread until it can be resumed. The OS thread associated with the suspended virtual thread is now free to perform operations for other virtual threads.

Virtual threads are implemented in a similar way to virtual memory. To simulate a lot of memory, an operating system maps a large virtual address space to a limited amount of RAM. Similarly, to simulate a lot of threads, the Java runtime maps a large number of virtual threads to a small number of OS threads.

Unlike platform threads, virtual threads typically have a shallow call stack, performing as few as a single HTTP client call or a single JDBC query. They support thread-local variables and inheritable thread-local variables, but you should carefully consider using them because a single JVM might support millions of virtual threads. Virtual threads are suitable for running tasks that spend most of the time blocked, often waiting for I/O operations to complete. However, they are not intended for long-running CPU-intensive operations.

When to Use Virtual Threads?

Use virtual threads in high-throughput concurrent applications, especially those that consist of a great number of concurrent tasks that spend much of their time waiting. Server applications are examples of high-throughput applications because they typically handle many client requests that perform blocking I/O operations such as fetching resources.

Virtual threads do not execute code faster than platform threads. Their goal is to provide scale (greater throughput) rather than speed (lower latency).



How to Create and Run a Virtual Thread?

There are two main ways to create and run a virtual thread in Java: using the Thread and Thread.Builder APIs, or using the Executors class.

1. Using the Thread and Thread.Builder APIs

You can use the Thread.ofVirtual() method to create an instance of Thread.Builder for creating virtual threads. You can also use the Thread.Builder methods to set the name, priority, daemon status, and uncaught exception handler of the virtual thread. Then, you can use the start method to start the virtual thread with a Runnable task. You can also use the Thread methods to join, interrupt, and check the state of the virtual thread.

For example: The below code creates and starts a virtual thread that prints “Hello, world!” to the standard output.

public class Main {

    public static void main(String[] args) throws InterruptedException {

        Thread virtualThread = Thread.ofVirtual().name("MyThread").start(() -> {
            System.out.println("Hello, world!");
        });

        Thread.sleep(2000);
    }
}
Java

Why Thread.sleep(2000) is mentioned in the above program?

The Java runtime does not guarantee that a virtual thread will run immediately after it is started. A virtual thread is scheduled to execute by the Java runtime as soon as possible, but it may be delayed or preempted by other threads or tasks. Therefore, the virtual thread may not have a chance to print Hello World on the console before the main thread exits.

To ensure that the virtual thread prints Hello World on the console, you need to wait for it to finish before the main thread exits. You can do this by calling the join method of the virtual thread object, which blocks the current thread until the virtual thread completes or you can even make use of Thread.sleep(ms) method.

2. Using the Executors Class

You can use the Executors.newVirtualThreadPerTaskExecutor() method to create an ExecutorService that launches a new virtual thread for each task. You can also use the ExecutorService methods to submit, execute, and manage the tasks.

For example: The below code creates and submits a task to a virtual thread executor that prints “Hello, world!” to the standard output.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {

    public static void main(String[] args) {
        // Create a virtual thread executor
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {

            // Submit a task to the executor
            executor.submit(() -> {
                System.out.println("Hello, world!");
            });

            // Shut down the executor
            executor.shutdown();
        }
    }
}
Java


Platform Threads vs Virtual Threads

Let’s see how Virtual Threads differ from Platform Threads

ParameterPlatform threadsVirtual threads
CreationCreated by using the Thread.ofPlatform() method or the default Thread constructorCreated by using the Thread.ofVirtual() method or the Executors.newVirtualThreadPerTaskExecutor() method
MappingDirectly mapped to the OS threads, and run Java code on their underlying OS threadsNot directly mapped to the OS threads, but managed and scheduled by the JVM, and run Java code on carrier threads
Resource consumptionConsume more memory and CPU resources, because they have large stacks and context switchesConsume less memory and CPU resources, because they have small stacks and no context switches
I/O blockingCan be blocked by I/O operations, and waste the OS thread while waitingCan be suspended and resumed by the JVM when they perform blocking I/O operations, and free the OS thread to run other virtual threads
APIsRequire using non-blocking or asynchronous APIs to avoid blocking and improve performance, which can complicate the codeCan use blocking or synchronous APIs without affecting performance, and simplify the code
Task suitabilitySuitable for running any kind of task, but they are a limited resourceNot suitable for running long-running CPU-intensive tasks, because they can starve other virtual threads on the same carrier thread
Native methodsCan call native methods that perform blocking or non-blocking operationsCannot call native methods that perform blocking operations, because they can block the carrier thread

How to Determine if a Thread is a Virtual Thread?

To determine if a thread is a virtual thread in Java, you can utilize the isVirtual() method from the Thread class. This method returns a boolean value, signaling whether the thread is a virtual thread or a platform thread. This method will yield true for virtual threads and false for platform threads.

For example, the following code creates and starts a virtual thread and a platform thread, and checks their types using the isVirtual() method:

Thread virtualThread = Thread.ofVirtual().start(() -> {
    System.out.println("Hello, world!");
});

Thread platformThread = Thread.ofPlatform().start(() -> {
    System.out.println("Hello, world!");
});

System.out.println("\nIs virtualThread a virtual thread? " + virtualThread.isVirtual());
System.out.println("Is platformThread a virtual thread? " + platformThread.isVirtual());
Java

The output of the code is:

Hello, world!
Hello, world!

Is virtualThread a virtual thread? true
Is platformThread a virtual thread? false
Java


Started vs Unstarted Virtual Threads

When you create a virtual thread in Java, you can choose to start it immediately or leave it unstarted. A started virtual thread is scheduled to execute by the Java runtime as soon as possible. An unstarted virtual thread is not scheduled to execute until you explicitly invoke its start method.

You can use the Thread and Thread.Builder APIs to create and start a virtual thread with a Runnable task. For example, the following code creates and starts a virtual thread that prints “Hello, world!” to the standard output:

Thread virtualThread = Thread.ofVirtual().start(() -> {
    System.out.println("Hello, world!");
});
Java

You can also use the Thread.ofVirtual().unstarted() method to create an unstarted virtual thread with a Runnable task. For example, the following code creates an unstarted virtual thread that prints “Hello, world!” to the standard output:

Thread virtualThread = Thread.ofVirtual().unstarted(() -> {
    System.out.println("Hello, world!");
});
Java

To start an unstarted virtual thread, you need to call its start method. For example, the following code starts the unstarted virtual thread created above:

virtualThread.start();
Java

You can also use the Thread.startVirtualThread method to create and start a virtual thread with a Runnable task in one step. For example, the following code creates and starts a virtual thread that prints “Hello, world!” to the standard output:

Thread virtualThread = Thread.startVirtualThread(() -> {
    System.out.println("Hello, world!");
});
Java


Java Virtual Threads in Action

Example 1: Creating and running 10,000 threads

This program creates and runs 10,000 threads that print “Hello, world!” to the standard output. It measures the time and memory used by the program.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {

    public static void main(String[] args) throws InterruptedException {
    
        // Creating a thread pool with virtual threads
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {

            long startMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
            long startTime = System.currentTimeMillis();

            for (int i = 0; i < 10_000; i++) {
                executor.submit(() -> {
                    System.out.println("Hello, world!");
                });
            }

            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);

            long endTime = System.currentTimeMillis();
            long endMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();

            long time = endTime - startTime;
            long memory = endMemory - startMemory;

            System.out.println("Time: " + time + " ms");
            System.out.println("Memory: " + memory + " bytes");
        }
    }
}
Java

If you run this program with platform threads, you will likely get an OutOfMemoryError or a very slow performance, because creating 10,000 OS threads is very expensive and resource-intensive.

If you run this program with virtual threads, you will get a much faster and lighter performance, because creating 10,000 virtual threads is very cheap and efficient. The Java runtime will use a small number of OS threads to execute the virtual threads.

Example 2: Adaptable Thread Behavior – Unbound from Specific OS Threads

The program demonstrates the use of virtual threads in Java, which are lightweight threads that are not bound to a specific OS thread. A virtual thread runs code on an OS thread, but when it encounters a blocking I/O operation, such as Thread.sleep(), the Java runtime suspends the virtual thread and frees the OS thread for other virtual threads. The ForkJoinPool assigns a different OS thread to the virtual thread when it is ready to run again.

public class Main {

    public static void main(String[] args) throws InterruptedException {

        Thread virtualThread = Thread.ofVirtual().start(() -> {
            System.out.println("Virtual thread started: " + Thread.currentThread());

            // Simulate a long-running task with multiple blocking points
            for (int i = 0; i < 15; i++) {
                System.out.println("Virtual thread working (iteration " + i + "): " + Thread.currentThread());
                try {
                    Thread.sleep(1000);  // Simulate 1-second blocking operation
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();  // Re-interrupt if needed
                }
            }

            System.out.println("Task completed on virtual thread: " + Thread.currentThread());
        });

        System.out.println("Main thread continuing...");

        System.out.println("Main thread waiting for virtual thread...");
        virtualThread.join();  // Wait for virtual thread completion

        System.out.println("Virtual thread finished.");
    }
}
Java

The output shows that the ForkJoinPool assigned two different worker threads to the virtual thread during its execution. This illustrates the flexibility and scalability of virtual threads.



What Is Virtual Thread Pinning?

Virtual thread pinning is a situation where a virtual thread cannot be detached from its underlying OS thread, also known as the carrier thread. This means that the virtual thread occupies the carrier thread for its entire lifetime, preventing other virtual threads from using it. This reduces the scalability and performance benefits of virtual threads.

Virtual thread pinning can happen for several reasons such as:

  1. Synchronized Blocks or Methods: When a virtual thread enters a synchronized block or method, it gets attached or locked to its carrier thread. This means the carrier thread can’t be used for other tasks while this block or method is running.
  2. Native Methods or Foreign Functions: If a virtual thread runs a native method or a foreign function, it also gets locked or pinned to that operation.

Detecting Virtual Thread Pinning

To detect and diagnose virtual thread pinning, you can use the -Djdk.tracePinnedThreads=full or -Djdk.tracePinnedThreads=short JVM options. These options enable the logging of pinning events. The full option prints more details than the short option, such as the stack trace.

You can specify these options as system properties in your code, or as command-line arguments when launching the JVM, such as java -Djdk.tracePinnedThreads=full.

For example, the below code showcases virtual thread pinning and detection wherein a synchronized task runs within a virtual thread, and the virtual thread gets pinned to its carrier thread.

public class Main {

    public synchronized void synchronizedTask() {
        try {
            Thread.sleep(100000);
            System.out.println("Work done by virtual thread. isVirtual: " + Thread.currentThread().isVirtual());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //System.setProperty("jdk.tracePinnedThreads", "full");

        Main main = new Main();
        Thread.startVirtualThread(main::synchronizedTask).join();
    }
}
Java

Output: -Djdk.tracePinnedThreads=full & -Djdk.tracePinnedThreads=short

javac Main.java                        
Java -Djdk.tracePinnedThreads=full Main

Thread[#21,ForkJoinPool-1-worker-1,5,CarrierThreads]
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183)
    java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
    java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)
    java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:793)
    java.base/java.lang.Thread.sleep(Thread.java:507)
    Main.synchronizedTask(Main.java:5) <== monitors:1
    java.base/java.lang.VirtualThread.run(VirtualThread.java:309)
Work done by virtual thread. isVirtual: true
Pinning Detection = Full
javac Main.java                        
Java -Djdk.tracePinnedThreads=short Main

Thread[#21,ForkJoinPool-1-worker-1,5,CarrierThreads]
    Main.synchronizedTask(Main.java:5) <== monitors:1
Work done by virtual thread. isVirtual: true

Pinning Detection = Short

Mystery of Empty Thread Names in Virtual Threads

Discover why virtual thread names often appear blank in logs or when using Thread.currentThread().getName():

Absence of Default Names:

Virtual threads, unlike conventional OS threads, lack predefined names from the system. This aligns with their lightweight design focused on rapid creation and management. Consequently, calling getName() on a virtual thread without an explicitly set name returns an empty string.

Setting Names Explicitly:

To assign a name to a virtual thread, utilize the setName() method or the Thread.builder().name() method. This can be useful for debugging or logging purposes, but you should not rely on the thread name for any logic or functionality in your code. You can use the Thread.isVirtual() method to check if the current thread is a virtual thread or a platform thread or if you need a unique identifier for a virtual thread, use its thread ID like : Thread.currentThread().threadId().



Virtual Threads vs Reactive Programming

Virtual threads and reactive programming are two different approaches to handle concurrency and scalability in Java applications. They both aim to reduce the overhead of blocking I/O operations and improve the utilization of system resources. However, they have different advantages and disadvantages, depending on the use case and the programming model. Here is a table that compares some of the key aspects of virtual threads and reactive programming:

ParameterVirtual ThreadsReactive Programming
ConceptLightweight threads that are not bound to a specific OS thread and can be suspended and resumed by the Java runtime when they perform blocking I/O operations.A paradigm that uses asynchronous, non-blocking, and event-driven streams of data to handle the complexity of concurrent applications.
Programming ModelImperative, sequential, and familiar to most Java developers.Functional, declarative, and requires a learning curve and a mindset shift.
Programming StyleYou write code as usual, using blocking APIs and libraries, and the JVM takes care of suspending and resuming the virtual threads when they are blocked.You write code using reactive APIs and libraries, such as WebFlux, that return data streams, such as Flux and Mono, that you can compose and transform.
AdvantagesSimplifies the development and maintenance of concurrent applications, reduces the memory footprint and increases the throughput of I/O-intensive applications, leverages existing blocking APIs and libraries.Provides fine-grained control and flexibility over concurrency and backpressure, enables composition and transformation of data streams, integrates with reactive frameworks and libraries.
DisadvantagesNot suitable for long-running CPU-intensive operations, may encounter compatibility issues with native methods or thread-local variables.Increases the complexity and verbosity of the code, requires careful error handling and testing, may encounter performance issues with blocking APIs or libraries

Things to Consider

Here are some things to consider when working with Java Virtual Threads:

  • No Default Names for Virtual Threads: Virtual threads lack default names. Assign names explicitly using setName() for clearer logs and easier debugging.
  • Thread IDs for Identification: Utilize the unique thread ID (Thread.currentThread().threadId()) for reliable identification when needed.
  • JVM Compatibility: Ensure Java 19 or later for utilizing virtual threads effectively.
  • Library and Framework Compatibility: Check the compatibility of third-party libraries and frameworks with virtual threads for seamless integration.
  • Debugging and Profiling Consideration: Adapt debugging and profiling tools and techniques to work efficiently with virtual threads.
  • Select Task Type Suitably: Employ virtual threads for I/O-bound tasks and consider traditional threads for CPU-intensive workloads.
  • Experiment and Stay Updated: Virtual threads are evolving. Stay updated and experiment to uncover their optimal use cases in your applications.

FAQs

How many virtual threads can be created?

What are the benefits of virtual threads?

What are the limitations of virtual threads?

Conclusion

The journey from processes to threads and the emergence of Java Virtual Threads showcases a remarkable evolution in task management within Java. The virtual threads revolutionize concurrency by providing a lightweight, scalable alternative, detached from specific OS threads. Their versatility and power promise a significant impact on Java’s concurrency landscape, shaping the way we approach programming in the future.

Learn More

#

Interested in learning more?

Check out our blog on gracefully handling UnsupportedOperationException in Java

Top Picks for Learning Java

Explore the recommended Java books tailored for learners at different levels, from beginners to advanced programmers.

Disclaimer: The products featured or recommended on this site are affiliated. If you purchase these products through the provided links, I may earn a commission at no additional cost to you.

1
Java: The Complete Reference
13th Edition

Java: The Complete Reference

  • All Levels Covered: Designed for novice, intermediate, and professional programmers alike
  • Accessible Source Code: Source code for all examples and projects are available for download
  • Clear Writing Style: Written in the clear, uncompromising style Herb Schildt is famous for
2
Head First Java: A Brain-Friendly Guide

Head First Java: A Brain-Friendly Guide

  • Engaging Learning: It uses a fun approach to teach Java and object-oriented programming.
  • Comprehensive Content: Covers Java's basics and advanced topics like lambdas and GUIs.
  • Interactive Learning: The book's visuals and engaging style make learning Java more enjoyable.
3
Modern Java in Action: Lambdas, streams, functional and reactive programming
2nd Edition

Modern Java in Action: Lambdas, streams, functional and reactive programming

  • Latest Java Features: Explores modern Java functionalities from version 8 and beyond, like streams, modules, and concurrency.
  • Real-world Applications: Demonstrates how to use these new features practically, enhancing understanding and coding skills.
  • Developer-Friendly: Tailored for Java developers already familiar with core Java, making it accessible for advancing their expertise.
4
Java For Dummies
8th Edition

Java For Dummies

  • Java Essentials: Learn fundamental Java programming through easy tutorials and practical tips in the latest edition of the For Dummies series.
  • Programming Basics: Gain control over program flow, master classes, objects, and methods, and explore functional programming features.
  • Updated Coverage: Covers Java 17, the latest long-term support release, including the new 'switch' statement syntax, making it perfect for beginners or those wanting to brush up their skills.

Add a Comment

Your email address will not be published.