kloia Blog

Benchmarking Java Virtual Threads: A Comprehensive Analysis

Written by Baran Gayretli | Aug 19, 2024 1:59:30 PM

A groundbreaking innovation called Java Virtual Threads (Project Loom) was designed to improve the concurrency mechanism and boost Java application performance. Since virtual threads are more lightweight than traditional threads, a larger number of concurrent threads can be managed more effectively.

What Are Java Virtual Threads?

Project Loom includes Java Virtual Threads in an effort to simplify the development, maintenance, and observation of high-throughput concurrent applications. Virtual threads are far lighter than traditional threads and can be created in large numbers without the overhead of system threads.

Why Use Virtual Threads?

- Scalability: The amount of concurrent operations your program is able to manage can be greatly increased by using virtual threads.

- Simplicity: Virtual threads simplify the concurrency model, which makes it easier to develop and maintain code.

- Performance: Virtual threads allow for better performance as a result of less resource consumption and context switching.

Benchmarking Java Virtual Threads

I will compare virtual threads with traditional threads to demonstrate their advantages. Here's how the benchmark can be set up.

Setting Up the Benchmark

  1. Environment:

       - JDK version supporting virtual threads (JDK 17+ with Project Loom enabled).

       - A benchmarking library like JMH (Java Microbenchmark Harness).

  2. Benchmark Code:

I will build a benchmarking test that compares the performance and resource efficiency of tasks carried out with traditional threads and virtual threads by measuring execution time, CPU load, and memory usage.

 

Sample Benchmark Code


import org.openjdk.jmh.annotations.*;

import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.util.concurrent.*;

@State(Scope.Benchmark)
public class ExtendedVirtualThreadsBenchmark {

    private static final int LIGHT_LOAD_TASK_COUNT = 1000;
    private static final int HEAVY_LOAD_TASK_COUNT = 100000;
    private OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();

    @Benchmark
    public void traditionalThreadsLightLoad() throws InterruptedException {
        runBenchmark(LIGHT_LOAD_TASK_COUNT, Executors.newFixedThreadPool(100));
    }

    @Benchmark
    public void virtualThreadsLightLoad() throws InterruptedException {
        runBenchmark(LIGHT_LOAD_TASK_COUNT, Executors.newVirtualThreadPerTaskExecutor());
    }

    @Benchmark
    public void traditionalThreadsHeavyLoad() throws InterruptedException {
        runBenchmark(HEAVY_LOAD_TASK_COUNT, Executors.newFixedThreadPool(100));
    }

    @Benchmark
    public void virtualThreadsHeavyLoad() throws InterruptedException {
        runBenchmark(HEAVY_LOAD_TASK_COUNT, Executors.newVirtualThreadPerTaskExecutor());
    }

    private void runBenchmark(int taskCount, ExecutorService executorService) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(taskCount);
        long startTime = System.nanoTime();
        double startCpuLoad = osBean.getSystemLoadAverage();
        long startMemoryUsage = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();

        for (int i = 0; i < taskCount; i++) {
            executorService.submit(() -> {
                // Simulate work
                performTask();
                latch.countDown();
            });
        }

        latch.await();
        executorService.shutdown();

        long endTime = System.nanoTime();
        double endCpuLoad = osBean.getSystemLoadAverage();
        long endMemoryUsage = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();

        System.out.println("Execution Time: " + (endTime - startTime) / 1_000_000 + " ms");
           System.out.println("CPU Load: " + (endCpuLoad - startCpuLoad));
        System.out.println("Memory Usage: " + (endMemoryUsage - startMemoryUsage) / (1024 * 1024) + " MB");
    }

    private void performTask() {
        // Simulate a task by sleeping
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Use JMH to run the benchmarks:


java -jar target/benchmarks.jar VirtualThreadsBenchmark


Visualization of the Results

Benchmark Comparison: Traditional Threads vs Virtual Threads

The benchmark results highlight the performance benefits of virtual threads, particularly under heavy load conditions.

- Execution Time: When compared to traditional threads, virtual threads offer a significant decrease in execution time, particularly in situations with high loads This demonstrates the efficiency and speed of virtual threads in handling a large number of concurrent tasks.

- CPU Load: Virtual threads use less CPU power than traditional threads do. This lower CPU load means more efficient processing and lower overhead related to thread management.

Memory Usage: Virtual threads require a significantly lower share of memory than traditional threads. This is particularly evident under heavy load, where virtual threads use nearly half the memory of traditional threads.

Key Takeaways and Practical Implications

For Java applications, Java Virtual Threads indicate an important advancement in concurrent programming. They simplify the development process and provide better performance and resource utilization. In terms of execution time, CPU load, and memory utilization, our benchmarks clearly demonstrate the benefits of virtual threads over traditional threads.

The findings have significant practical results: 

- Simplified Concurrency Management: Without having to worry about the overhead and complexity of using traditional threads, developers may design code that is easier to understand and maintain.

- Improved Application Performance: This is important for apps that depend on performance since it allows for greater throughput and reduced latency.

- Resource Efficiency: Improved resource utilization translates to cost savings in both development and production environments, making virtual threads an economically attractive choice.

By adopting Java Virtual Threads, developers can create highly scalable and efficient applications with reduced complexity. This benchmark provides concrete evidence of the benefits of virtual threads, making a strong case for their use in modern Java development.

References