Java Virtual Threads: A Case Study
What virtual threads / event-loop patterns try to optimize
- Main target: throughput and hardware utilization for highly concurrent, mostly I/O‑bound workloads.
- Goal is to keep OS threads busy doing useful work instead of blocking on I/O.
- They reduce per‑thread memory overhead (no fixed large kernel stack per unit of concurrency) and the number of kernel context switches.
- They allow “thread per request” models without exhausting OS threads.
Developer experience vs async / callbacks
- Strong theme: writing linear, blocking-style code is easier to reason about, debug, and manage resources in (stack traces, try/finally, RAII).
- Async/callback/evented styles are described as leaky, hard to debug, and requiring “function coloring” (different APIs for async vs sync).
- Virtual threads promise async-level scalability with a simple, familiar threading API.
- Some argue this hides the concurrency hazards of multi-threading and removes explicit guardrails Futures/Tasks provide.
Performance trade‑offs and overheads
- Several commenters note JVM adds its own scheduler and continuation machinery: mounting/unmounting virtual threads, copying stacks to heap, GC pressure.
- Context switching doesn’t disappear; it moves from kernel to JVM, though per-switch cost may be much lower.
- Virtual threads often shine with huge numbers of mostly waiting tasks; for CPU‑bound workloads or modest concurrency, platform threads can match or beat them.
- One linked study suggests surprising performance issues in current Java implementation, especially for relatively small thread counts.
Limits, pitfalls, and “thread pinning”
- Virtual threads share the same memory model as normal threads; data races and synchronization issues remain.
- Known pitfalls: blocking in native code, synchronized methods, and some file I/O can “pin” a carrier OS thread, negating benefits.
- Lack of time-slice preemption for virtual threads today means long-running CPU work can still starve others unless carefully managed.
- Excessive ThreadLocal use or legacy libraries not adapted to virtual threads can degrade memory usage and behavior.
Comparisons to other models and ecosystems
- Compared to Go goroutines, Erlang processes, Kotlin coroutines, C# async/await, and Node.js: virtual threads aim for Go/Erlang-like concurrency with full Java compatibility.
- Some see Java’s approach (plus structured concurrency JEP) as cleaner than C#’s async ecosystem split; others argue async/await offers powerful explicit composition patterns that green threads alone don’t replace.
- Reactive frameworks and non-blocking JDBC are seen as less compelling when you can get similar throughput with simpler virtual-thread-based imperative code, though reactive/event-loop models still suit some niches.
Benchmarks and skepticism
- Multiple commenters question benchmarks where virtual threads underperform, noting unrepresentative workloads (CPU-bound, few blocking calls, auto-growing thread pools).
- Consensus: benefits appear mainly for large numbers of blocking I/O tasks; using virtual threads for everything without workload analysis can disappoint.