Showing posts with label Thread. Show all posts
Showing posts with label Thread. Show all posts

Platform vs. Virtual Threads

This blog post is a raw comparison of platform and virtual threads. We will use both the types of threads and see how they behave and perform.

It is important to note that the execution time and other matrics may vary system to system, however, it can definitely give a rough idea about the overall picture. We will try to keep this comparison as fair as possible, if I fail to do so, feel free to post comment below.

My System

  • MacBook Pro
    • RAM 16 GB
    • Processor 2.6 GHz 6-Core Intel Core i7
  • Java 21

Time to start

Platform Threads : 21 ms

private static void platform() {
    long start = System.currentTimeMillis();
    for(int i = 0; i < 100; i++) {
        Thread t = new Thread(() -> System.out.println(Thread.currentThread().getThreadGroup().getName()));
        t.start();
    }
    long end = System.currentTimeMillis();
    System.out.println("Platform : " + (end - start) + " ms");
}

Virtual Threads : 18 ms

private static void virtual() {
    long start = System.currentTimeMillis();
    for(int i = 0; i < 100; i++) {
        Thread.startVirtualThread(() -> System.out.println(Thread.currentThread().getThreadGroup().getName()));
    }
    long end = System.currentTimeMillis();
    System.out.println("Virtual : " + (end - start) + " ms");
}

For 100 Threads the difference doesn’t look significant. However, If I increase the count of threads from 100 to 1000 the difference increases drastically.

  • Platform Threads : 235 ms
  • Virtual Threads : 29 ms

Reason

Even though the time taken to initialize threads are different, underlying operation would have taken same amount of time. Virtual threads enables better utilisation of the resources compared platform threads (virtual thread actually uses platform threads internally).

The major difference here is that the platform thread managed by OS are blocking, but virtual thread which are managed by JVM are non-blocking. So, in case of virtual thread it will wait for the platform thread to be available but won’t block the execution of the program.

Speed

Platform : 24 ms

private static void platform() {
    Thread t = new Thread(() -> {
        long start = System.currentTimeMillis();
        for(int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getThreadGroup().getName());
        }
        long end = System.currentTimeMillis();
        System.out.println("Platform : " + (end - start) + " ms");
    });
    t.start();
}

In case of virtual thread, there is no output in console. Looks like it didn’t print anything to console, the thread didn’t start.

private static void virtual() {
    Thread.startVirtualThread(() -> {
        long start = System.currentTimeMillis();
        for(int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getThreadGroup().getName());
        }
        long end = System.currentTimeMillis();
        System.out.println("Virtual : " + (end - start) + " ms");
    });
}

It seems we need to use join here.

private static void virtual() throws InterruptedException {
    Thread.startVirtualThread(() -> {
        long start = System.currentTimeMillis();
        for(int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getThreadGroup().getName());
        }
        long end = System.currentTimeMillis();
        System.out.println("Virtual : " + (end - start) + " ms");
    }).join();
}

And it worked, but took comparatively more time than platform thread, almost double.

Virtual : 49 ms

Let’s try to be fair here,

private static void platform() throws InterruptedException {
    Thread t = new Thread(() -> {
        long start = System.currentTimeMillis();
        for(int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getThreadGroup().getName());
        }
        long end = System.currentTimeMillis();
        System.out.println("Platform : " + (end - start) + " ms");
    });
    t.start();
    t.join();
}

Platform : 26 ms

For some reason, platform thread is winning here.

I thought of replacing System.out.println(Thread.currentThread().getThreadGroup().getName()); with System.out.println("."); to keep it simpler.

If we run both methods together from the main method,

public class Test {

    public static void main(String[] args) throws InterruptedException {
        platform();
        virtual();
    }

    private static void virtual() throws InterruptedException {
        Thread.startVirtualThread(() -> {
            long start = System.currentTimeMillis();
            for(int i = 0; i < 1000; i++) {
                System.out.println(".");
            }
            long end = System.currentTimeMillis();
            System.out.println("Virtual : " + (end - start) + " ms");
        }).join();
    }

    private static void platform() throws InterruptedException {
        Thread t = new Thread(() -> {
            long start = System.currentTimeMillis();
            for(int i = 0; i < 1000; i++) {
                System.out.println(".");
            }
            long end = System.currentTimeMillis();
            System.out.println("Platform : " + (end - start) + " ms");
        });
        t.start();
        t.join();
    }
}

Multiple executions,

  • Platform : 19 ms and Virtual : 15 ms
  • Platform : 21 ms and Virtual : 24 ms
  • Platform : 21 ms and Virtual : 16 ms
  • Platform : 22 ms and Virtual : 14 ms
  • Platform : 22 ms and Virtual : 29 ms

After changing the sequence of methods the result changes dramatically,

...
public static void main(String[] args) throws InterruptedException {
    virtual();
    platform();
}
...

Multiple executions,

  • Platform : 17 ms and Virtual : 60 ms
  • Platform : 7 ms and Virtual : 35 ms
  • Platform : 10 ms and Virtual : 47 ms
  • Platform : 9 ms and Virtual : 35 ms
  • Platform : 6 ms and Virtual : 30 ms

Okay, lot of confusion here. Let’s find answers of each one by one.

Reason

Why virtual thread couldn’t execute?

In first case the problem is that the main thread exits before the virtual thread completes it’s execution. We can definitely join the virtual thread to our main thread which we did. Again the non blocking behaviour made it behave this way.

public static void main(String[] args) throws InterruptedException {
    virtual();
    Thread.sleep(50000);
}

This gives Virtual : 33 ms

Why platform thread executed faster with join?

In case of virtual threads JVM has to deal with additional work underneath, which leads to slight delay in the execution. Even though it’s one thread and we are joining it to main, compared to platform thread it took more time to finish. This ovehead may include things like park/unpark the task based on JVM or CPU bound decision making.

How sequence of method is impacting the thread task execution time?

Again the blocking nature of platform thread makes life of virtual thread easier. For platform thread executed first the JVM, CPU or I/O is managed differently compared to virtual thread if executed first. Since the task is pretty simple the impact is significantly lower but such differences can make developer wonder if not understood properly.

Virtual Threads : Java 21

Java 21 came with an exciting feature of Virtual Thread - Lightweight Threads. The idea is to achieve optimal hardware utilisation, which has been a bottleneck for the conventional java.lang.Thread - platform threads.

Platform threads are 1:1 mapped with OS Thread, which makes them powerful to accomplish all possible tasks. java.lang.Thread is a thin layer on actual OS threads, which are quite heavy. The number of thread application can support depends on the hardware capacity of the system.

Let’s say thread memory consumption is 1 MB, to support 1 million threads in concurrent application at the time of heavy load, you will need 1 TB of memory. One thread per request style of implementation also suffer due to this limitation - asynchronous programming can solve this upto some extent but it has its own drawbacks.

Virtual threads effectively shares the platform thread. Rather than holding the platform thread for entire lifetime, it runs short lived tasks and for the needed executions - not while it waits for I/O. This allows insane number of concurrent operations without need of additional threads or hardware capacity. This brings up the new way to deal with concurrency in Java applications.

Virtual threads can be created with java.lang.Thread builder approach,

Runnable function = () -> System.out.println("Something to execute in Virtual Thread!");
Thread virtual = Thread.startVirtualThread(function);

In addition to that, java.util.concurrent.ExecutorService also has factory method for Virtual threads,

ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();

Virtual threads are daemon threads, join() to wait on the main thread. Thread local variables of virtual thread are not accessible to carrier thread or vice versa.