Skip to Content
Course content

113: Virtual Threads and Structured Concurrency

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why should I care about Virtual Threads if I already have Thread Pools?

For years, we've been told that platform threads (the ones mapped 1:1 to OS threads) are expensive. Each one eats up about 1MB of memory for its stack. If you try to spin up 10,000 platform threads to handle 10,000 concurrent API requests, your JVM will likely crash with an OutOfMemoryError long before you hit that number. That's why we relied on ExecutorService and fixed thread pools—we were essentially rationing a very scarce resource.

Virtual threads change the game because they are managed by the JVM, not the OS. They are incredibly lightweight. I've seen apps scale to millions of virtual threads on a single machine without breaking a sweat. The "magic" is that when a virtual thread hits a blocking I/O operation (like a database call or an HTTP request), the JVM detaches it from the underlying platform thread and puts it aside until the I/O is done. This means your CPU is always doing real work instead of idling while waiting for a network packet. You can finally write simple, synchronous-looking code that performs like complex asynchronous code.

How do I actually use them in a real-world scenario?

You don't need to learn a whole new API. Most of the time, you just swap out your executor. Let's say you're building a dashboard that needs to fetch data from three different microservices: User Profile, Order History, and Loyalty Points. Instead of chaining CompletableFuture calls—which, let's be honest, can become a nightmare to read—you can just use a virtual thread per task.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var profileFuture = executor.submit(() -> fetchUserProfile(userId));
    var ordersFuture = executor.submit(() -> fetchOrderHistory(userId));
    var pointsFuture = executor.submit(() -> fetchLoyaltyPoints(userId));

    // These calls block, but they don't block the OS thread!
    var profile = profileFuture.get();
    var orders = ordersFuture.get();
    var points = pointsFuture.get();
    
    return new Dashboard(profile, orders, points);
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException("Failed to aggregate dashboard data", e);
}

Notice the try-with-resources block. The new virtual thread executor implements AutoCloseable, meaning it will wait for all submitted tasks to finish before closing. It's clean, it's readable, and it scales.

What is Structured Concurrency, and why is it better than just "firing and forgetting"?

In traditional concurrency, if you spawn a thread to do a sub-task and that thread hangs or the main request is cancelled, that sub-task often keeps running in the background. We call these "orphan threads," and they are a primary cause of memory leaks and weird production bugs.

Structured Concurrency treats a group of related tasks as a single unit of work. If one task fails, the others are automatically cancelled. In Java, this is handled via StructuredTaskScope. I prefer this approach because it brings the "scope" concept to multi-threading: the parent thread cannot exit until all its children have finished.

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask user = scope.fork(() -> fetchUserProfile(userId));
    Subtask fetchOrderHistory(userId));

    scope.join();           // Wait for all tasks
    scope.throwIfFailed();  // If any task failed, throw the exception and cancel others

    return new Dashboard(user.get(), orders.get());
}

Are there any "gotchas" I need to be aware of?

Yes, and this is the one that trips up most senior devs: Pinning. A virtual thread is "pinned" to its carrier (the platform thread) if it executes a synchronized block or a native method. When a virtual thread is pinned, it can't be detached during I/O, which means you lose the whole benefit of virtual threads. If you pin too many threads, you're right back to the platform thread bottleneck.

If you're migrating an old codebase to virtual threads and notice performance dipping, check your synchronized blocks. My advice? Replace them with ReentrantLock. ReentrantLock does not pin the virtual thread, allowing the JVM to park it and move on to other work while waiting for the lock to be released.




📋 Practical Task

Exercise: Build a Resilient Flight Search Aggregator

You are tasked with building a flight search tool that aggregates prices from three different airline APIs (BudgetAir, GlobalFly, and SkyHigh). To ensure a fast user experience, you must implement the following requirements using Java's Virtual Threads and Structured Concurrency:

  • Requirement 1: Use a StructuredTaskScope.ShutdownOnFailure to trigger three concurrent requests.
  • Requirement 2: Simulate network latency using Thread.sleep() in each request (between 100ms and 1000ms).
  • Requirement 3: Implement a "Fail-Fast" mechanism: if any one of the airline APIs throws an IOException, all other pending requests should be cancelled immediately.
  • Requirement 4: Ensure the final result is only returned if all three services respond successfully.

Starter Code Hint: Define a FlightPrice record to hold the airline name and the price, and a FlightSearchService class that contains the searchAllAirlines(String destination) method implementing the scope logic.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.