Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
113: Virtual Threads and Structured Concurrency
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.ShutdownOnFailureto 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.
There are no comments for now.