With the introduction of virtual threads in Java 21, it is now possible to optimize the usage of the operating system threads by avoiding blocking them for asynchronous operations. Furthermore, virtual threads’s instantiation has very little overhead and they can be created in large quantities. This means that it can be more efficient to use them over the default platform threads for tasks that involve I/O or some other blocking operations.

Why is this an issue?

Whenever a virtual thread is started, the JVM will mount it on an OS thread. As soon as the virtual thread runs into a blocking operation like an HTTP request or a filesystem read/write operation, the JVM will detect this and unmount the virtual thread. This allows another virtual thread to take over the OS thread and continue its execution.

This is why virtual threads should be preferred to platform threads for tasks that involve blocking operations. By default, a Java thread is a platform thread. To use a virtual thread it must be started either with Thread.startVirtualThread(Runnable) or Thread.ofVirtual().start(Runnable).

This rule raises an issue when a platform thread is created with a task that includes heavy blocking operations.

How to fix it

Replace platform thread instances or platform thread pools with virtual threads, if their task involves blocking operations.

Code examples

Noncompliant code example

The following example creates a platform thread to handle a blocking operation, here denoted by Thread.sleep(1000). The overhead for instantiating a platform thread is higher than for a virtual thread. Further, instantiating too many platform threads can lead to problems if the number of instantiated threads exceeds the maximum number of platform threads allowed by the OS.

new Thread(() -> {
    try {
        Thread.sleep(1000); // Noncompliant blocking operation in platform thread
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
});

In the context of thread pools, using platform threads for heavy blocking operations can lead to the thread pool running out of available threads. Even though the threads spend most of their time waiting for e.g. I/O operations to complete and subsequently the CPU usage is low, the application cannot continue processing efficiently, due to the lack of available threads.

Compliant solution

Using virtual threads allows the developer to abstract from any pooling logic as they are much lighter than platform threads, and the number of virtual threads that can be instantiated is only limited by the available memory. In this example, the execution of 10000 requests would take just over ~1 second without any risk of exceeding the allowed number of platform threads.

Thread.ofVirtual().start(() -> {
    try {
        Thread.sleep(1000); // Compliant
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
});

Resources

Documentation