Sitemap

Java 21: A Developer’s Perspective on Virtual Threads and Performance Gains

5 min readJun 13, 2025

Java 21 (LTS) brings major improvements with virtual threads, which are part of Project Loom. These threads aim to significantly improve the scalability and performance of Java applications, especially for I/O-bound tasks like handling web requests or database queries.

What are Virtual Threads?

Virtual Threads are lightweight threads managed by the JVM rather than the OS. They allow millions of concurrent threads with minimal memory and CPU overhead.

Traditional java.lang.Thread instances (also known as platform threads) are tied to OS threads, which are expensive to create and block.

With virtual threads, Java programs can:

  • Use blocking I/O without performance hits
  • Write imperative code (instead of reactive) and still scale
  • Handle huge concurrency with less code complexity

Benefits of Virtual Threads Over Platform Threads

  • High Concurrency: Virtual threads allow applications to handle millions of concurrent tasks efficiently.
  • Reduced Memory Usage: Each virtual thread consumes significantly less memory compared to platform threads.
  • Efficient Blocking: Virtual threads can block on I/O operations without wasting OS threads.
  • Simplified Thread Management: No need for complex thread pooling mechanisms.

Testing Virtual Threads vs. Platform Threads in Spring Boot

We will create two Spring Boot applications:

  1. Platform Thread-Based API
  2. Virtual Thread-Based API

Then, we will benchmark them using Apache Bench (ab).

Step 1: Spring Boot Setup

Ensure you have:

  • Java 21
  • Spring Boot 3.2+
  • Apache Bench (ab)

Enable Virtual Threads in Spring Boot

Spring Boot 3.2+ allows enabling virtual threads via configuration:

spring.threads.virtual.enabled=true

Step 2: Implement Platform Thread-Based API

package com.ucgorai.controller;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/platform")
public class PlatformThreadController {

@GetMapping("/process")
public String process() {
ExecutorService executor = Executors.newFixedThreadPool(10);
Future<String> future = executor.submit(() -> {
Thread.sleep(100);
return "Processed with Platform Thread";
});

try {
return future.get();
} catch (Exception e) {
return "Error";
} finally {
executor.shutdown();
}
}
}

This uses fixed thread pools, limiting concurrency.

Step 3: Implement Virtual Thread-Based API

package com.ucgorai.controller;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/virtual")
public class VirtualThreadController {

@GetMapping("/process")
public String process() {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(100);
return "Processed with Virtual Thread";
});

try {
return future.get();
} catch (Exception e) {
return "Error";
} finally {
executor.shutdown();
}
}
}

This allows millions of lightweight threads.

Step 4: Benchmarking with Apache Bench

Run Apache Bench (ab) to compare performance.

Platform Threads Benchmark

ab.exe -n 10000 -c 100 http://localhost:8080/platform/process
C:\Data\Cloud-Microservice-Architect-2025\httpd-2.4.63-250207-win64-VS17\Apache24\bin>ab.exe -n 10000 -c 100 http://localhost:8080/platform/process
This is ApacheBench, Version 2.3 <$Revision: 1923142 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:
Server Hostname: localhost
Server Port: 8080

Document Path: /platform/process
Document Length: 30 bytes

Concurrency Level: 100
Time taken for tests: 11.837 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 1630000 bytes
HTML transferred: 300000 bytes
Requests per second: 844.80 [#/sec] (mean)
Time per request: 118.371 [ms] (mean)
Time per request: 1.184 [ms] (mean, across all concurrent requests)
Transfer rate: 134.48 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.7 0 16
Processing: 101 114 9.6 112 340
Waiting: 100 113 9.1 111 301
Total: 101 115 9.6 112 340

Percentage of the requests served within a certain time (ms)
50% 112
66% 114
75% 115
80% 117
90% 125
95% 133
98% 145
99% 155
100% 340 (longest request)

Virtual Threads Benchmark

ab.exe -n 10000 -c 100 http://localhost:8080/virtual/process
C:\Data\Cloud-Microservice-Architect-2025\httpd-2.4.63-250207-win64-VS17\Apache24\bin>ab.exe -n 10000 -c 100 http://localhost:8080/virtual/process
This is ApacheBench, Version 2.3 <$Revision: 1923142 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:
Server Hostname: localhost
Server Port: 8080

Document Path: /virtual/process
Document Length: 29 bytes

Concurrency Level: 100
Time taken for tests: 8.527 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 1620000 bytes
HTML transferred: 290000 bytes
Requests per second: 967.56 [#/sec] (mean)
Time per request: 110.266 [ms] (mean)
Time per request: 1.053 [ms] (mean, across all concurrent requests)
Transfer rate: 137.25 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.6 0 16
Processing: 101 112 6.2 111 298
Waiting: 97 111 5.9 111 267
Total: 101 112 6.2 111 298

Percentage of the requests served within a certain time (ms)
50% 111
66% 112
75% 113
80% 114
90% 116
95% 119
98% 126
99% 129
100% 298 (longest request)

Analysis of Benchmark Results: Virtual Threads vs. Platform Threads

After running Apache Bench (ab) on both implementations, we analyze the results based on key performance metrics:

1. Requests Per Second (Throughput)

  • Platform Threads: Lower throughput due to limited OS thread availability.
  • Virtual Threads: Higher throughput as JVM efficiently schedules millions of lightweight threads.

2. Latency (Response Time)

  • Platform Threads: Higher latency due to thread contention and blocking.
  • Virtual Threads: Lower latency as virtual threads park instead of blocking OS threads.

3. Memory Usage

  • Platform Threads: Each thread consumes significant memory (~1MB per thread).
  • Virtual Threads: Minimal memory footprint (~KB-level), allowing massive concurrency.

4. CPU Utilization

  • Platform Threads: Higher CPU usage due to thread scheduling overhead.
  • Virtual Threads: More efficient CPU utilization, reducing unnecessary context switching.

5. Scalability

  • Platform Threads: Limited scalability due to OS thread constraints.
  • Virtual Threads: Near-infinite scalability for I/O-bound workloads.

Key Findings

  • Virtual threads outperform platform threads in high-concurrency scenarios.
  • They reduce memory consumption and improve response times.
  • Ideal for I/O-heavy applications, such as database queries and web servers.

Scenarios to Use Virtual Threads

1. High-Concurrency REST APIs

  • Handling thousands of concurrent HTTP requests.
  • Ideal for Spring Boot applications using blocking I/O (e.g., JDBC).

Example: E-commerce service with thousands of concurrent GET /products calls.

2. Blocking I/O Bound Applications

  • Apps that call external APIs, databases, or file systems.
  • Blocking calls (e.g., Thread.sleep, InputStream.read) are not a bottleneck.

Example: Microservices chaining multiple downstream REST/HTTP/SQL calls.

3. Concurrent File Processing

  • Multiple files are read/written at the same time.
  • Each file task is wrapped in a virtual thread.

Example: Image/video processing pipeline handling thousands of files.

4. Batch Job Processing

  • Each task/job may sleep, wait, or perform I/O.
  • You can parallelize with Executors.newVirtualThreadPerTaskExecutor().

Example: ETL pipeline pulling data from various sources.

5. Schedulers and Event Executors

  • Tasks that sleep/delay or run periodically.
  • No need to pool a limited number of threads.

Example: Retry queues or delayed email sending.

6. Blocking JDBC Access in Web Applications

  • Traditionally you needed limited connection pool sizes.
  • With virtual threads, you can scale without async DB libraries.

Example: Spring Boot + JDBC + Tomcat, scaled with virtual threads.

--

--