Java runtime execution models: standard vs virtual threads vs reactive
"In this blog post, I analyze most common Java runtime executions models, evaluate their use cases and pros & cons of each one"
I’ve been observing Java apps in production environments for 5+ years now — and under heavy load, deciphering concurrency behavior often felt like solving a mystery.
It’s time to demystify the most prominent runtime execution models in Java and clearly understand their architectural characteristics, trade-offs, strengths, and weaknesses — especially under pressure.
Let’s break it down.
There are 3 major threading models: standard, virtual threads, and reactive (actors, etc. are out of scope).
We analyze them for different workload types.
Standard#
Fixed-size pool of platform threads (1–2MB stack per thread). Tasks queue up in a blocking queue.
Maximum concurrency = thread pool size.
❌ IO-bound: Threads block on IO → pool exhausts → queue builds → GC can’t keep up → eventually OutOfMemoryError, unless queue is explicitly bounded → requests rejected. CPU underutilized.
❌ CPU-bound: Threads saturate CPU → context switching overhead → tasks slow down → queue grows → OOM again. CPU hits 100%.
Virtual Threads (Project Loom, Java 21+)#
Lightweight threads (~KB stack), mounted on a small carrier pool. Managed by the JVM’s scheduler.
Supports hundreds of thousands of concurrent tasks.
✅ IO-bound: Excellent fit. Virtual threads park during IO and release carriers. CPU stays light, carriers active. No blocking queues needed. Scales gracefully.
❌ CPU-bound: Virtual threads still depend on limited carrier threads. With too many CPU-heavy tasks, carriers get overwhelmed → virtual threads stall waiting for a slot → latency rises. Context switching hurts. CPU hits 100%, but JVM stays stable (no OOM).
Reactive (via engine like Netty)#
Few threads (often one per core), each running an event loop. Tasks are non-blocking, cooperative, and must return control quickly.
Backpressure is essential.
✅ IO-bound, non-blocking: Event loops remain unblocked, and tasks are efficiently scheduled. Throughput is very high. Low memory footprint, low latency.
❌ CPU-bound: Can struggle badly. Long-running ops block the loop → loop handles one task at a time → internal queue grows → no drain → likely OOM. CPU hits 100%. You must offload to a worker pool — adds complexity.
Surprisingly, as of 2025, platform threads are still the default — likely due to inertia in the Java ecosystem.
But virtual threads are quickly taking over for request-response services: they offer the same simplicity with far better scalability, much like Kotlin and Go. They’ve also replaced many reactive use cases like high-load DB access.
However, when it comes to app-level streaming — think receiving NATS events and pushing updates via RSocket, WebSocket, or SSE — the reactive model remains unbeatable.