Project Loom virtual threads replace platform threads. 10,000 concurrent requests without thread pool. Production ready Java 21+.
JOptimize Team
Virtual threads change everything about Java concurrency.
Traditional Java: 1 request = 1 platform thread. 10,000 requests = 10,000 threads. Each thread = 1MB stack = 10GB memory.
Virtual threads: 1 request = 1 virtual thread. 10,000 requests = 1 thread pool of 100 platform threads. Each virtual thread = few KB = 50MB total.
Traditional servlet:
@GetMapping("/api/user/{id}") public User getUser(@PathVariable String id) { User user = userService.getUser(id); // Blocks thread return user; }
At 10,000 concurrent requests:
Exact same code works differently:
@GetMapping("/api/user/{id}") public User getUser(@PathVariable String id) { User user = userService.getUser(id); // Blocks virtual thread, not platform thread return user; }
When code hits I/O (database, HTTP call, sleep), virtual thread is suspended.
Platform thread runs other virtual threads.
No explicit async needed. Code is simple and readable.
10,000 requests
↓
10,000 platform threads (each 1MB stack)
↓
Context switch every 10ms
↓
GC pressure
↓
CPU cache misses
↓
→ Throughput: 100-1000 req/sec
10,000 requests
↓
10,000 virtual threads (each 1-10KB)
↓
100 platform threads (actual workers)
↓
Virtual thread blocks → park in heap
↓
Platform thread runs next virtual thread
↓
When I/O completes → resume virtual thread
↓
→ Throughput: 10,000+ req/sec (100x improvement)
Concurrent connections: 10,000 Memory used: 10GB+ Context switches/sec: 100,000+ Throughput: 500 req/sec P99 latency: 5000ms (5 seconds)
Concurrent connections: 10,000 Memory used: 50MB Context switches/sec: Minimal (no OS switching) Throughput: 50,000 req/sec (100x) P99 latency: 50ms (0.05 seconds)
# application.properties spring.threads.virtual.enabled=true
That's it. Spring boot automatically uses virtual threads.
@Configuration public class VirtualThreadConfig { @Bean public ExecutorService virtualThreadExecutor() { return Executors.newVirtualThreadPerTaskExecutor(); } }
// Still uses platform threads ExecutorService pool = Executors.newFixedThreadPool(10);
Before (Platform Threads):
@Component public class EventConsumer { @Autowired private ExecutorService executor; // 50 threads @KafkaListener(topics = "events", concurrency = "50") public void handle(String message) { executor.submit(() -> { // Process message // 50 threads, so max 50 concurrent }); } }
Result: 50 threads, 50MB stack = limited concurrency
After (Virtual Threads):
@Component public class EventConsumer { @Autowired private ExecutorService executor; @KafkaListener(topics = "events") public void handle(String message) { // Virtual thread per message // No thread pool needed process(message); } }
Result: 10,000 concurrent messages, 50MB total memory
// Bad: ThreadLocal doesn't work well across virtual threads private static final ThreadLocal<User> currentUser = new ThreadLocal<>(); // Good: Use ScopedValue (Java 21+) private static final ScopedValue<User> currentUser = ScopedValue.newInstance();
ThreadLocal was tied to OS threads. Virtual threads need ScopedValue.
// Bad: Blocks platform thread (pinning) synchronized (lock) { // Virtual thread blocks, but so does platform thread // Defeats the purpose } // Good: Use ReentrantLock lock.lock(); try { // Virtual thread blocks, platform thread continues } finally { lock.unlock(); }
Synchronized blocks cause pinning - virtual thread pins platform thread. Avoid.
Older libraries may assume 1 thread per request. They work but don't get full benefits.
Modern frameworks (Spring 6.1+, Quarkus, etc.) optimize for virtual threads.
spring.threads.virtual.enabled=trueVirtual threads are production-ready in Java 21+.
Same code, 100x throughput. No async/await plumbing.
ChangeP99 latency from seconds to milliseconds.
Smallest change for biggest impact in Java history.
Optimize with JOptimize PRO. Use code LINKEDIN40 for 40% OFF.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.