Sitemap
Aryash Jain

Aryash Jain

Software Engineer.

Java Virtual Threads: Java 21 Vs Java 25

5 min readOct 2, 2026

--

Part 1: Understanding Virtual Threads

Java Virtual Threads represent one of the most significant innovations in concurrent programming since the introduction of the Java Memory Model. If you’re building high-performance, scalable applications in Java, you need to understand this technology.

Virtual Threads are lightweight threads managed by the JVM that make it easier to write, maintain, and observe high-throughput concurrent applications.

1. What are Virtual Threads?

The Problem with Platform Threads
Traditional Java threads (called platform threads) are OS-level threads with significant drawbacks:

  • Heavy memory footprint (~1–2 MB per thread)
  • Expensive context switching
  • Limited scalability (typically thousands of threads before problems)
  • OS scheduler has limited visibility into your application

Here’s an example of traditional thread limitations:

// Traditional approach — Creating many platform threads
ExecutorService executor = Executors.newFixedThreadPool(1000);

for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
try {
Thread.sleep(1000);
System.out.println("Task " + i + " completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}

executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);

The problem with this approach: significant memory pressure and context switching overhead.

Virtual Threads: The Solution

Virtual threads are lightweight abstractions over OS threads that solve these problems.

Architecture Overview:

  • Virtual Threads: 100,000+ (User-level)
  • JVM Scheduler: Manages Virtual Threads
  • Platform Threads: A few hundred (OS-level)
  • OS Scheduler: Manages Platform Threads

Key Characteristics Comparison:

Press enter or click to view image in full size

2. How to Initialize and Use Virtual Threads

Method 1: Using Thread.ofVirtual()

Thread vThread = Thread.ofVirtual()
.name("my-virtual-thread")
.start(() -> {
System.out.println("Running in a virtual thread!");
System.out.println("Thread: " + Thread.currentThread().getName());
});

vThread.join();

### Method 2: ExecutorService with Virtual Threads

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

for (int i = 0; i < 10000; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " running on " + Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}

executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
System.out.println("All tasks completed!");

Method 3: Thread Factory

ThreadFactory virtualThreadFactory = Thread.ofVirtual()
.name("worker-", 0)
.factory();

ExecutorService executor = Executors.newThreadPerTaskExecutor(virtualThreadFactory);
executor.submit(() -> System.out.println("Executed by virtual thread!"));
executor.shutdown();

### Best Places to Use Virtual Threads

IDEAL FOR:

  • Server applications (REST APIs, microservices)
  • High I/O workloads (database queries, API calls)
  • Applications with many concurrent connections
  • Legacy code with blocking I/O

NOT IDEAL FOR:

  • CPU-intensive tasks (computation-heavy work)
  • Low-latency, real-time systems
  • Tasks that hold locks for long periods

3. Scalability & Performance Benefits

### Performance Comparison: Virtual vs Platform Threads

Scenario: 10,000 HTTP Requests with 100ms Latency

public class PerformanceComparison {

public static void platformThreadApproach() throws Exception {
long startTime = System.currentTimeMillis();
ExecutorService executor = Executors.newFixedThreadPool(200);

for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}

executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
long duration = System.currentTimeMillis() - startTime;
System.out.println("Platform Threads Time: " + duration + "ms");
}

public static void virtualThreadApproach() throws Exception {
long startTime = System.currentTimeMillis();
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}

executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
long duration = System.currentTimeMillis() - startTime;
System.out.println("Virtual Threads Time: " + duration + "ms");
}

public static void main(String[] args) throws Exception {
System.out.println("=== Platform Threads ===");
platformThreadApproach();

System.out.println("\n=== Virtual Threads ===");
virtualThreadApproach();
}
}

Expected Results:

- Platform Threads Time: 5000ms

Get Aryash Jain’s stories in your inbox

Join Medium for free to get updates from this writer.

- Virtual Threads Time: 1200ms

### Real-world REST API Example

@RestController
@RequestMapping("/api")
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.fetchUserFromDatabase(id);
List<Order> orders = userService.fetchUserOrders(id);
user.setOrders(orders);
return ResponseEntity.ok(user);
}

@GetMapping("/users/batch/{count}")
public ResponseEntity<List<User>> getBatchUsers(@PathVariable int count) {
List<User> users = IntStream.range(1, count + 1)
.parallel()
.mapToObj(userService::fetchUserFromDatabase)
.collect(Collectors.toList());

return ResponseEntity.ok(users);
}
}

— -

4. Issues with Virtual Threads in Java 21

Issue #1: Pinning to Platform Threads

Virtual threads can get “pinned” to their underlying platform thread, preventing efficient scheduling.

public class PinningExample {
private static final Object LOCK = new Object();

public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

for (int i = 0; i < 100; i++) {
executor.submit(() -> {
synchronized (LOCK) { // Synchronization causes pinning!
try {
Thread.sleep(1000);
System.out.println("Task completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}

executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
}
}

Why Its Bad ?

  • Virtual thread remains bound to platform thread
  • Defeats the purpose of having thousands of virtual threads
  • Other virtual threads can’t use that platform thread
  • Performance degrades significantly

Issue #2: No Support for ThreadLocal Inheritance

ThreadLocal<String> context = new ThreadLocal<>();
context.set("user-123");

Thread vThread = Thread.ofVirtual().start(() -> {
String value = context.get(); // Returns null in Java 21!
System.out.println("Context: " + value);
});

Issue #3: Limited Observability

Java 21 had limited built-in observability:

- No easy way to monitor how many virtual threads are running

- Limited thread utilization metrics

- Difficulty getting detailed stack traces for debugging

Issue #4: WeakHashMap Performance

Virtual threads in Java 21 had issues with WeakHashMap due to frequent garbage collection pauses caused by the large number of thread-related objects.

5. How These Issues Are Resolved in Java 25

Solution #1: Improved Pinning Behavior

Java 25 significantly reduces pinning issues. Use ReentrantLock instead of synchronized:

public class ImprovedPinning {
private static final ReentrantLock LOCK = new ReentrantLock();

public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

for (int i = 0; i < 100; i++) {
executor.submit(() -> {
LOCK.lock();
try {
Thread.sleep(1000);
System.out.println("Task completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
LOCK.unlock();
}
});
}

executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
}
}

Key improvements in Java 25:

- ReentrantLock no longer causes pinning

- ReentrantReadWriteLock fully supports virtual threads

- Reduced unnecessary pinning in core JDK classes

### Solution #2: Enhanced ThreadLocal Support

ThreadLocal<String> context = new ThreadLocal<>();
context.set("user-123");

Thread vThread = Thread.ofVirtual().start(() -> {
String value = context.get(); // Now returns "user-123"!
System.out.println("Context: " + value);
});

What changed:

- Virtual threads now properly inherit ThreadLocal values

- Better support for context propagation

- Improved integration with existing code patterns

Solution #3: Better Observability and Monitoring

import java.lang.management.ManagementFactory;
import com.sun.management.ThreadMXBean;

public class VirtualThreadMonitoring {
public static void main(String[] args) {
ThreadMXBean threadBean = (ThreadMXBean) ManagementFactory.getThreadMXBean();
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}

System.out.println("Virtual threads: " + threadBean.getVirtualThreadCount());
System.out.println("Platform threads: " + threadBean.getThreadCount());
System.out.println("CPU time: " + threadBean.getCurrentThreadCpuTime());

executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
}
}

Solution #4: Improved GC Performance

- Better memory management for ThreadLocal and related structures

- Reduced GC pauses with large numbers of virtual threads

- Optimized object pooling and reuse

8. Conclusion

### Key Takeaways

Benchmarking has significantly improved with virtual threads , we are using it in our high QPS services , and they are really helping us in servicing highly concurrent requests without CPU overheads

If this article helped you in diving deep , please give a Clap and also comment what next should be served next , it can be related to Java , Virtual Threads or Any new programming concept.

--

--