Java Thread Performance Vs. Virtual Threads Part 2

Java-threads-and-virtual-threads-performance-benchmark Image generated by DALL-E

In previous posts, here and here, I did a very simple performance benchmark between Java’s Thread vs. Java’s Virtual Threads and Kotlin’s coroutines. My fiend Hossein asked me about benchmarking with tasks that have heavy memory and performance usage to have a better insights. Here I am trying to change the benchmark test, so instead of just sleeping for a few seconds inside each job, this time, I want to do a really higher memory and CPU intensive tasks.

First Round

In first round, I changed the code to copy a 1,000,000 integers array and then iterate throw the array to find a specific value.

This is job runnable code:

Runnable task = new Runnable() {
    @Override
    public void run() {
      try {
        boolean found = false;
        int[] numbersCopy = Arrays.copyOf(numbers, numbers.length);
        for (int i = 0; i < numbersCopy.length; i++) {
          if (numbersCopy[i] == 1000000) {
            found = true;
          }
        }
      } finally {
        latch.countDown(); // decrement the count of the latch
      }
    }
  };

This is result for 10,000 threads:

Time taken with regular threads: 5059ms
Time taken with virtual threads: 2478ms

Virtual thread has better performance. It is about two times faster.

Now, I will try with 50,000 threads:

Time taken with regular threads: 17092ms
Time taken with virtual threads: 12289ms

The performance of virtual threads, is still better than Threads but is less than the previous run.

Lets do the test with 100,000 threads to see the result:

Time taken with regular threads: 34063ms
Time taken with virtual threads: 24385ms

Still better performance for virtual threads!

For 200,000 threads:

Time taken with regular threads: 65082ms
Time taken with virtual threads: 48078ms

For 400,000 threads:

Time taken with regular threads: 128880ms
Time taken with virtual threads: 95860ms

And for 1,000,000 threads:

Time taken with regular threads: 351020ms
Time taken with virtual threads: 253580ms

This is benchmark chart:

Thread duration and virtual threads duration chart

And this is complete source code:

package info.behzadian;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class Main {

  public static void main(String[] args) throws InterruptedException {
    Main main = new Main();
    main.startTest();
  }

  CountDownLatch latch;
  private int[] numbers;

  private void startTest() throws InterruptedException {
    int numThreads = 1_000_000;
    latch = new CountDownLatch(numThreads);
    numbers = createRandomNumbersArray();

    long start = System.currentTimeMillis();
    for(int i =0; i < numThreads; i++) {
      new Thread(task).start();
    }
    latch.await(); // wait for all threads to finish
    long end = System.currentTimeMillis();
    System.out.println("Time taken with regular threads: " + (end - start) + "ms");

    latch = new CountDownLatch(numThreads); // reset the latch
    start = System.currentTimeMillis();
    for(int i =0; i < numThreads; i++) {
      Thread.startVirtualThread(task);
    }
    latch.await(); // wait for all virtual threads to finish
    end = System.currentTimeMillis();
    System.out.println("Time taken with virtual threads: " + (end - start) + "ms");
  }

  Runnable task = new Runnable() {
    @Override
    public void run() {
      try {
        boolean found = false;
        int[] numbersCopy = Arrays.copyOf(numbers, numbers.length);
        for (int i = 0; i < numbersCopy.length; i++) {
          if (numbersCopy[i] == 1000000) {
            found = true;
          }
        }
      } finally {
        latch.countDown(); // decrement the count of the latch
      }
    }
  };

  private static int[] createRandomNumbersArray() {
    int[] numbers = new int[1000000];
    Random random = new Random();
    for (int i = 0; i < numbers.length; i++) {
      numbers[i] = 1 + random.nextInt(Integer.MAX_VALUE - 1);
    }

    return numbers;
  }

}

Second Round

Now, I want to see the performance comparison between threads and virtual threads on a CPU intencive task. I create a random generated long values array with size 1,000 and then in each run, we iterate over this array and check item to see if the number is Palindromic number.

This is new job runnable code:

  Runnable task = new Runnable() {
    @Override
    public void run() {
      try {
        int palindromeCount = 0;
        for (long num : numbers) {
          if (isPalindrome(num)) {
            palindromeCount++;
          }
        }
      } finally {
        latch.countDown(); // decrement the count of the latch
      }
    }
  };

And this a function to see if a long number is a palindromic number:

  public boolean isPalindrome(long num) {
    long reversed = 0, remainder, original = num;
    while (num != 0) {
      remainder = num % 10;
      reversed = reversed * 10 + remainder;
      num /= 10;
    }
    return original == reversed;
  }

This is result for 10,000 threads:

Time taken with regular threads: 1003ms
Time taken with virtual threads: 124ms

Now, I will try with 50,000 threads:

Time taken with regular threads: 3655ms
Time taken with virtual threads: 418ms

Lets do the test with 100,000 threads to see the result:

Time taken with regular threads: 6560ms
Time taken with virtual threads: 805ms

For 200,000 threads:

Time taken with regular threads: 14899ms
Time taken with virtual threads: 1891ms

For 400,000 threads:

Time taken with regular threads: 26038ms
Time taken with virtual threads: 3242ms

And for 1,000,000 threads:

Time taken with regular threads: 65596ms
Time taken with virtual threads: 9821ms

This is benchmark chart:

Thread duration and virtual threads duration chart

It is very clear that for tasks that needs a CPU processing, virtual thread has better performance than threads.

And this is complete source code:

package info.behzadian;

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class Main {

  public static void main(String[] args) throws InterruptedException {
    Main main = new Main();
    main.startTest();
  }

  CountDownLatch latch;
  long[] numbers;

  private void startTest() throws InterruptedException {
    int numThreads = 10_000;
    latch = new CountDownLatch(numThreads);
    numbers = createRandomLongArray();

    long start = System.currentTimeMillis();
    for(int i =0; i < numThreads; i++) {
      new Thread(task).start();
    }
    latch.await(); // wait for all threads to finish
    long end = System.currentTimeMillis();
    System.out.println("Time taken with regular threads: " + (end - start) + "ms");

    latch = new CountDownLatch(numThreads); // reset the latch
    start = System.currentTimeMillis();
    for(int i =0; i < numThreads; i++) {
      Thread.startVirtualThread(task);
    }
    latch.await(); // wait for all virtual threads to finish
    end = System.currentTimeMillis();
    System.out.println("Time taken with virtual threads: " + (end - start) + "ms");
  }

  Runnable task = new Runnable() {
    @Override
    public void run() {
      try {
        int palindromeCount = 0;
        for (long num : numbers) {
          if (isPalindrome(num)) {
            palindromeCount++;
          }
        }
      } finally {
        latch.countDown(); // decrement the count of the latch
      }
    }
  };

  private long[] createRandomLongArray() {
    long[] numbers = new long[1_000];
    Random random = new Random();

    for (int i = 0; i < numbers.length; i++) {
      numbers[i] = 1 + random.nextLong(Long.MAX_VALUE - 1);
    }

    return numbers;
  }

  public boolean isPalindrome(long num) {
    long reversed = 0, remainder, original = num;
    while (num != 0) {
      remainder = num % 10;
      reversed = reversed * 10 + remainder;
      num /= 10;
    }
    return original == reversed;
  }

}