Understanding Virtual Threads in Java 21

Understanding Virtual Threads in Java 21

Boost Performance in Java Apps with Virtual Threads in Java 21

Wait, don’t we already have Threads in Java?

To start with virtual threads, it's imperative to understand the existing Threads API in Java very briefly.

Till Java 19, all the JVM Threads had a direct mapping with an underlying native OS thread which was tied to the corresponding JVM thread till that task wasn't over. What this implies is that if we ended up doing a blocking I/O with a JVM thread, we'd end up blocking the native OS thread which chokes up our processor and makes the system heavily non-performant.

Way before Java 19, a project, known as Project Loom was initiated by OpenJDK with the sole purpose of developing a highly efficient and lightweight concurrency model in Java.

With Java 19, they rolled out a preview feature known as Virtual Threads. With the birth of a new type of thread, the original threads came to be known as Platform Threads.

So, what exactly are Virtual Threads?

As we now know, Platform Threads had a huge issue with performance as well as a high chance of choking up resources. Plus, when we're dealing with native threads on the OS level, context switching takes a toll on application performance too.

Next, we can only spawn so many native threads before eating up our memory, and today's world deals with millions of requests per second which is only going to increase as we fly toward a world of unimaginable speeds and technical innovations at a very high speed.

To deal with this, we have a brand new way in Java to create threads called Virtual Threads. With Virtual Threads, we can create threads in Java that reside in JVM and are not mapped one-to-one with native OS threads. Instead, these are lightweight JVM managed threads that map to an OS thread when needed and can be detached when blocked to unblock the already limited OS threads.

But How?

Answer to this is pretty simple. Platform Threads still exist that map to OS threads. But these shiny new Virtual Threads reside as an abstraction of task that can be mounted/unmounted on to Platform threads while the current execution waits and resources can be utilized by another task. That's what brings out the best performance out of them. This makes it one-to-many mapping between Platform Threads and Virtual Threads.

To understand better, take a look below.

This is how it looked with only Platform Threads

This is how it looks now with Virtual Threads


Great! So how do we create these Virtual Threads?

The shortest way to create a new Virtual Thread is to use the updated Thread API. It gives you the option to create one out of the Virtual v/s Platform thread. Here's how to do it.

    public static void main(String[] args) {
        Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hey I'm A Thread"));
        try {
            thread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

With the updated java.lang.Thread API, we get two methods called ofVirtual() & ofPlatform() which, as the name suggests, generate either a virtual or a platform thread.

Does Executor Framework support Virtual Threads?

Well, it would have been really disappointing if our favorite threads management framework had no support for turbocharging our good old threads. So yes, the executor framework can generate virtual threads. Here's how it works.

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<String> callable = () -> "Hello World From Thread";

        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        Future<String> future = executor.submit(callable);

        while (!future.isDone()) {}

        System.out.println(future.get());
    }

Here pay close attention to the method we called to get our Executor Service, Executors.newVirtualThreadPerTaskExecutor()

This is the method that creates a ThreadFactory of Virtual Threads. The JDK code for this method is :

    public static ExecutorService newVirtualThreadPerTaskExecutor() {
        // This is where it generates a ThreadFactory of VTs
        ThreadFactory factory = Thread.ofVirtual().factory();
        return newThreadPerTaskExecutor(factory);
    }

But are they really efficient in comparison to Platform Threads?

After all the efforts we put to create the threads in brand new way, is it even worth it?

To confirm how efficient are Virtual Threads, I ran a pretty simple test to compare them.

Looping over 10K integers and printing them by spawning a new Thread. One int per Thread. Here's the code for both of them.

Generating 10k Virtual Threads

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

        long startTime = System.nanoTime();
        // Generating 10K Virtual Threads
        for(int i=1; i<=1_00_00; i++) {
            Thread t = Thread.ofVirtual().name("virtual-"+i).start(() -> {
                System.out.println(Thread.currentThread().getName() );
            });
            t.join();
        }

        long endTime = System.nanoTime();
        long elapsedTime = endTime - startTime;
        double elapsedTimeInMillis = elapsedTime / 1_000_000.0;
        double elapsedTimeInSeconds = elapsedTimeInMillis / 1_000;

        System.out.println("Elapsed time in seconds: " + elapsedTimeInSeconds);

    }

Generating 10k Platform Threads

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

        long startTime = System.nanoTime();
        // Generating 10K Virtual Threads
        for(int i=1; i<=1_00_00; i++) {
            Thread t = Thread.ofPlatform().name("platform-"+i).start(() -> {
                System.out.println(Thread.currentThread().getName() );
            });
            t.join();
        }

        long endTime = System.nanoTime();
        long elapsedTime = endTime - startTime;
        double elapsedTimeInMillis = elapsedTime / 1_000_000.0;
        double elapsedTimeInSeconds = elapsedTimeInMillis / 1_000;

        System.out.println("Elapsed time in seconds: " + elapsedTimeInSeconds);

    }

Here's the result :

Thread TypeTime Taken for 10K (in seconds)
Platform Threads0.58267
Virtual Threads0.27964

This little comparison already shows that Virtual Threads are twice as fast as Platform Threads. But I saved the fun part for the last. How about we try a more realistic number. In real life scenario, threads are supposed to handle millions of requests. So how about we run the same piece of code for 1 million tasks.

Aaandd here comes the fun part! ** drum roll****

Thread TypeTime Taken for 1M (in seconds)
Platform Threads42.5807
Virtual Threads14.2660

WOAHH!! Thrice as fast!! And this is just for a simple integer loop!

Making your system (or atleast a part of it) 3X more efficient has never been more straightforward and simple!!


Virtual Threads sound great! Is this the first time something like this is created?

Well, no.

This isn't the first of its kind implementation. Infact, Java has been a bit late to jump wagon of using this pattern to utilise memory by removing 1-to-1 mapping with OS threads. Languages like Go and Kotlin have already been providing their implementations of Virtual Threads.

Learning from these languages, future seems bright for this shiny new tech in our beloved Java. Let's make the most out of our memory and try punch a hole in the sky.

Till next time :)