Introduction: The Concurrency Revolution
Java's concurrency model has undergone its most significant transformation since java.util.concurrent in Java 5. Project Loom — finalised in Java 21 LTS — introduces virtual threads, structured concurrency, and scoped values that fundamentally change how developers build concurrent applications.
Traditional reactive frameworks (Reactor, RxJava) solved the scalability problem but introduced callback complexity, debugging nightmares, and steep learning curves. Virtual threads achieve the same throughput with imperative, blocking-style code that reads linearly and debugs naturally. This guide covers virtual thread internals, structured concurrency patterns, Spring Boot integration, performance benchmarks, and migration strategies for production Java applications.
Virtual Thread Architecture and Internals
Understand how virtual threads work under the hood:
- Continuation-Based Model: Virtual threads are implemented as continuations — when a virtual thread blocks on I/O, the JVM unmounts it from the carrier (platform) thread and stores its stack in heap memory. When the I/O completes, the continuation resumes on any available carrier thread. This enables millions of concurrent virtual threads on a handful of platform threads.
- Carrier Thread Pool: Virtual threads execute on a ForkJoinPool of carrier threads (default size = available processors). The scheduler multiplexes virtual threads onto carriers — when one blocks, another immediately takes its place. This eliminates the idle-thread waste of traditional thread-per-request models.
- Stack Memory: Platform threads allocate ~1MB stack memory each (OS-managed). Virtual threads use dynamically-sized heap-allocated stacks — starting at just a few KB and growing as needed. This reduces memory footprint by 100–1000x for I/O-heavy workloads.
- Pinning: Virtual threads can become "pinned" to carrier threads when executing inside
synchronizedblocks or native methods — the carrier thread can't be reused while pinned. Monitor pinning with-Djdk.tracePinnedThreads=fulland migratesynchronizedtoReentrantLockto eliminate pinning. - Thread Identity: Each virtual thread has its own thread ID, name, and interrupt status — they're full
Threadinstances. Existing code usingThread.currentThread(), thread interruption, and thread-based debugging works unchanged with virtual threads.
Structured Concurrency and Scoped Values
Build reliable concurrent code with structured patterns:
- StructuredTaskScope: Group related concurrent tasks so they succeed or fail as a unit.
ShutdownOnFailurecancels all subtasks when any fails — preventing resource waste.ShutdownOnSuccessreturns the first successful result and cancels remaining tasks — perfect for redundant service calls. - Error Propagation: Structured concurrency propagates exceptions naturally — if a subtask throws, the scope catches it, cancels siblings, and re-throws in the parent. No more lost exceptions in callback chains or unhandled CompletableFuture failures that silently swallow errors.
- Scoped Values (JEP 481): Replace ThreadLocal with ScopedValue for virtual thread-safe context propagation. ScopedValues are immutable within a scope, automatically cleaned up when the scope exits, and inherited by child virtual threads. Use for request context (user ID, trace ID, locale) that flows through the call stack.
- Task Decomposition: Structure concurrent work as task trees — parent tasks spawn child subtasks that run concurrently within a scope. The parent waits for all children, handles their results, and ensures cleanup. This mirrors try-with-resources semantics for concurrent operations.
- Cancellation Propagation: When a scope is cancelled (timeout, failure, or explicit cancellation), all subtasks receive interruption. Virtual threads respond to interruption on any blocking operation — I/O, sleep, lock acquisition — ensuring prompt cancellation without polling loops.
Spring Boot and Framework Integration
Leverage virtual threads in Spring Boot 3.2+ applications:
- Auto-Configuration: Enable virtual threads with a single property:
spring.threads.virtual.enabled=true. Spring Boot automatically configures Tomcat/Jetty to use virtual threads for request handling — each HTTP request runs on its own virtual thread instead of a pooled platform thread. - Servlet Container: Tomcat 10.1+ with virtual threads handles thousands of concurrent requests without thread pool exhaustion. Traditional Tomcat limits at ~200 threads (200MB memory); with virtual threads, the same server handles 10,000+ concurrent connections with minimal memory overhead.
- Database Access: JDBC with virtual threads is naturally non-blocking — virtual threads unmount during database I/O, freeing carrier threads for other work. HikariCP connection pool sizing becomes the bottleneck, not thread count. Consider increasing pool size or using PgBouncer for connection multiplexing.
- WebClient Migration: Applications using WebClient for non-blocking I/O can migrate to synchronous RestClient with virtual threads — achieving the same throughput with simpler, debuggable code. RestClient + virtual threads = WebClient throughput + imperative simplicity.
- Transaction Management: Spring's
@Transactionalworks seamlessly with virtual threads. Transaction context flows through ScopedValues. For concurrent database operations within a transaction, use StructuredTaskScope to coordinate multiple queries while maintaining ACID guarantees.
Performance Benchmarks and Real-World Results
Quantified virtual thread performance advantages:
- Throughput: REST API benchmark (Spring Boot 3.2, PostgreSQL, 1000 concurrent users): Platform threads (200 pool) — 2,400 req/s, virtual threads — 8,700 req/s (3.6x improvement). The bottleneck shifts from thread count to database connection pool and actual I/O latency.
- Memory: 10,000 concurrent connections: Platform threads consume ~10GB (1MB × 10K); virtual threads consume ~50MB (5KB × 10K). This 200x reduction enables running high-concurrency workloads on smaller instances, cutting cloud infrastructure costs by 60–80%.
- Latency: P99 latency under load: Platform threads (200 pool, 1000 concurrent) — 850ms (queueing); virtual threads — 45ms (no queueing). Virtual threads eliminate request queueing because there's no thread pool to exhaust — every request gets its own thread immediately.
- Startup: Application startup with virtual threads is 15–20% faster — Spring's bean initialization runs on virtual threads, parallelising I/O-heavy startup tasks (database schema validation, cache warming, health checks).
- CPU-Bound Caveat: For CPU-bound workloads (computation, serialisation, encryption), virtual threads provide no benefit — carrier thread count limits parallelism. Use platform threads with ForkJoinPool for CPU-intensive work and virtual threads for I/O-intensive work.
Transform Your Publishing Workflow
Our experts can help you build scalable, API-driven publishing systems tailored to your business.
Migration Strategy from Reactive to Loom
Migrate existing reactive codebases incrementally:
- Phase 1 — Assessment: Identify reactive code patterns — Mono/Flux chains, WebClient calls, reactive repositories. Categorise by complexity: simple chains (direct migration), complex orchestration (requires restructuring), and streaming (may stay reactive). Profile I/O vs CPU distribution.
- Phase 2 — Infrastructure: Upgrade to Java 21 LTS, Spring Boot 3.2+, and compatible libraries. Enable virtual threads and verify existing code works unchanged. Monitor for pinning issues with
-Djdk.tracePinnedThreads. Replacesynchronizedblocks with ReentrantLock where pinning occurs. - Phase 3 — Incremental Migration: Start with leaf services — convert reactive endpoints to imperative one at a time. Replace
WebClientwithRestClient, reactive repositories with blocking JPA/JDBC, and Mono/Flux chains with sequential code using StructuredTaskScope for parallel operations. - Phase 4 — Testing: Load test migrated endpoints against reactive originals — verify equivalent throughput, reduced latency, and lower memory usage. Compare error handling behaviour — ensure exceptions propagate correctly without reactive error operators.
- Phase 5 — Keep Reactive Where Appropriate: Streaming use cases (SSE, WebSocket, infinite sequences) may benefit from reactive streams. Backpressure-heavy scenarios (rate-limited consumers) work naturally with reactive publishers. Hybrid architectures — virtual threads for request handling, reactive streams for event processing — are perfectly valid.
Observability and Production Monitoring
Monitor virtual thread behaviour in production:
- Java Flight Recorder (JFR): JFR captures virtual thread events — creation, blocking, unblocking, pinning, and termination. Analyse thread lifecycle to identify long-running virtual threads, excessive blocking, and pinning hotspots. Enable with
-XX:StartFlightRecording=filename=recording.jfr. - Micrometer Metrics: Track virtual thread metrics — active count, queued tasks, carrier thread utilisation, and task completion rate. Export to Prometheus/Grafana for dashboards showing concurrency patterns and capacity utilisation.
- Thread Dumps: Virtual thread dumps include thousands of threads — use
jcmd <pid> Thread.dump_to_file -format=jsonfor structured output. Filter by thread state (BLOCKED, WAITING) to identify bottlenecks. Group by stack trace to find common blocking points. - Distributed Tracing: Micrometer Tracing and OpenTelemetry work with virtual threads — trace context propagates through ScopedValues. Spans show actual I/O wait time rather than thread pool queueing, providing accurate latency attribution for each service call.
- Health Indicators: Monitor carrier thread pool saturation, virtual thread creation rate, pinning frequency, and GC pressure from heap-allocated stacks. Alert on carrier pool exhaustion (all carriers pinned) and excessive virtual thread creation (memory pressure).
Conclusion and MDS Java Services
Project Loom represents the most significant evolution in Java concurrency — delivering reactive-level throughput with imperative code simplicity. Key takeaways:
- Use virtual threads for I/O-bound work — HTTP requests, database queries, file operations. Platform threads for CPU-bound computation.
- Adopt structured concurrency — StructuredTaskScope and ScopedValues replace error-prone CompletableFuture chains with reliable, cancellable task trees.
- Migrate incrementally — Convert reactive endpoints one at a time, validate with load testing, and keep reactive streams where backpressure and streaming are genuinely needed.
- Monitor production carefully — Track pinning, carrier utilisation, and virtual thread lifecycle with JFR and Micrometer.
MetaDesign Solutions provides expert Java modernisation services — from Project Loom migration and virtual thread adoption through Spring Boot 3.2+ upgrades, reactive-to-imperative codebase transformation, structured concurrency implementation, performance benchmarking, and production monitoring setup for enterprise Java applications.



