Java 21 virtual threads Project Loom tutorial with examples — Complete Guide
A practical, in-depth guide to Java 21 virtual threads Project Loom tutorial with examples with examples.
INTRO
If you’ve ever written a Java service that spawns hundreds of blocking I/O calls—database queries, HTTP requests, file reads—you know the pain of thread pool exhaustion. Traditional platform threads are heavyweight; each one consumes a megabyte of stack memory and incurs a non‑trivial context‑switch cost. When the number of concurrent requests climbs, you either over‑provision the JVM or start seeing latency spikes and out‑of‑memory errors.
Project Loom flips that model on its head. Starting with Java 21, virtual threads give you the illusion of a one‑thread‑per‑request architecture while the runtime multiplexes millions of lightweight fibers onto a handful of carrier threads. The result is code that looks synchronous, stays readable, and scales without the usual thread‑management gymnastics. In this teaser we’ll surface why virtual threads matter for everyday microservices and what you’ll need to change (or not change) in your existing codebase to reap the benefits.
WHAT YOU'LL LEARN
- How virtual threads differ from platform threads and why they’re safe for blocking I/O.
- The minimal API changes required to convert a classic
ExecutorService‑based pipeline to a virtual‑thread‑backed one. - Real‑world patterns: parallel HTTP calls, database batch processing, and reactive‑style streams using only the JDK.
- Debugging and monitoring tips: JFR events,
Thread.dumpStack(), and tooling support in modern IDEs. - Common pitfalls—blocking the carrier pool, uncontrolled thread creation, and how to avoid them.
- Production‑ready configuration: sizing carrier pools, integrating with Spring Boot, and graceful shutdown.
A SHORT CODE SNIPPET
import java.net.http.*;
import java.util.concurrent.*;
import java.util.List;
public class VirtualThreadDemo {
private static final HttpClient client = HttpClient.newHttpClient();
public static void main(String[] args) throws Exception {
// Create a virtual‑thread executor
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Callable<String>> tasks = List.of(
() -> client.sendAsync(HttpRequest.newBuilder()
.uri(URI.create("https://example.com/a"))
.GET()
.build(), HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body).join(),
() -> client.sendAsync(HttpRequest.newBuilder()
.uri(URI.create("https://example.com/b"))
.GET()
.build(), HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body).join()
);
// Run both calls concurrently on virtual threads
List<Future<String>> results = executor.invokeAll(tasks);
results.forEach(r -> System.out.println(r.get()));
}
}
}
The snippet shows a drop‑in replacement for a traditional thread pool: newVirtualThreadPerTaskExecutor() creates a new virtual thread for each task, letting you fire off many blocking HTTP calls without worrying about pool saturation.
KEY TAKEAWAYS
- Virtual threads let you write straightforward, blocking code while the JVM handles massive concurrency under the hood.
- Switching to virtual threads often requires only a single line change (
Executors.newVirtualThreadPerTaskExecutor()), keeping your existing business logic intact. - Properly configuring carrier threads and avoiding blocking operations on them is crucial to prevent performance regressions.
- The JDK now ships with built‑in observability for virtual threads, making it feasible to adopt Loom in production without third‑party agents.
👉 Read the complete guide with step-by-step examples, common mistakes, and production tips:
Java 21 virtual threads Project Loom tutorial with examples — Complete Guide
Top comments (0)