Java Thread Performance Vs. Virtual Threads Part 2
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:
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:
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;
}
}