Java 21 – Virtual Threads

Java supports concurrency.

You can perform multiple tasks at the same time in a program.

And how does Java implement this?

Using Threads!

A thread is the basic unit of concurrency in Java.

If you want to perform multiple tasks in parallel you create one thread for each task and perform them in parallel.

And Java internally created one thread per one Operating System thread.

So if the operating system supported 1000 threads for example , you could run only 1000 threads in parallel.

If you had submitted more than 1000 tasks in parallel , they will have to wait until the current threads finish their jobs.

This has been a limitation in Java.

Not only that , OS threads are heavyweight (they occupy a lot of memory) and expensive (a lot of internal calls are made)

Java came up with Virtual Threads to the rescue.

Virtual Threads are not mapped one on one to OS threads.

Multiple virtual threads could be mapped to an OS thread internally.

From a developer perspective , we can now create millions of threads even though the underlying OS supports much lesser number of threads!

And how do you create virtual threads?

It is as simple as changing a single method name.

For example ,

Let’s say we want to invoke 100,000 threads in parallel.

Without virtual threads we would be creating those threads using :

Executor.newCachedThreadPool()

or using

Executor.newFixedThreadPool(1000)

In the latter method we specify the maximum number of threads that can be created. So if there are more than the specified number , the additional requests will have to wait.

Here is an example code which uses Executor.newCachedThreadPool().

import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

public class App {
    public static void main(String a[]) {

        long start = System.currentTimeMillis();
        try (var executor = Executors.newCachedThreadPool())) {
            IntStream.range(0, 100000).forEach(i -> {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofSeconds(1));
                    return i;
                });
            });
        }

        long end = System.currentTimeMillis();
        

        System.out.println("Duration :" + (end - start) / 1000);

    }
}

Each thread just sleeps for one second and then return.

The above code took 17 seconds to run in my machine

I then used Executor.newFixedThreadPool(1000) to run the same tasks:

import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

public class App {
    public static void main(String a[]) {

        long start = System.currentTimeMillis();
        try (var executor = Executors.newFixedThreadPool(1000)) {
            IntStream.range(0, 100000).forEach(i -> {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofSeconds(1));
                    return i;
                });
            });
        }

        long end = System.currentTimeMillis();

        System.out.println("Duration :" + (end - start) / 1000);

    }
}

The above code took 101 seconds to execute.

This is because I restricted the maximum number of threads that can be used to 1000.

And finally I created virtual threads using

Executor.newVirtualThreadPerTaskExecutor()
    import java.time.Duration;
    import java.util.concurrent.Executors;
    import java.util.stream.IntStream;

    public class App {
        public static void main(String a[]) {

            long start = System.currentTimeMillis();
            try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
                IntStream.range(0, 100000).forEach(i -> {
                    executor.submit(() -> {
                        Thread.sleep(Duration.ofSeconds(1));
                        return i;
                    });
                });
            }

            long end = System.currentTimeMillis();

            System.out.println("Duration :" + (end - start) / 1000);

        }
    }

The above code just took 5 seconds!

Much faster.

And there is only a single line of code change required!

Java has abstracted all the changes so from a developer perspective there is nothing to learn new to use virtual threads except that they need to call a new method now.

Note that,

Virtual threads can use Thread Local Variables just like platform threads.

Virtual threads can throw error stack traces the same way like platform threads.

Virtual threads can be debugged the same way as platform threads.

Virtual threads can be monitored using Java Flight Recorder the same way as platform threads.

Also ,

Virtual threads are not faster than platform threads, they just provide more scalability. (You can create more virtual threads than platform threads)

Virtual threads cannot be pooled /should not be pooled since they are huge in number and inexpensive , so you can create one thread per application request.

That’s it!

Reference:

https://openjdk.org/jeps/444


Posted

in

, ,

by

Comments

Leave a Reply

Discover more from The Full Stack Developer

Subscribe now to keep reading and get access to the full archive.

Continue reading