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
90: Virtual Threads (Project Loom)
For years, we've been taught that threads are expensive. If you've spent any time tuning a production server, you know the drill: you create a fixed thread pool, maybe 200 threads, and you pray that your incoming request rate doesn't spike high enough to exhaust that pool. The moment your threads are all blocked waiting for a database query or a slow third-party API, your application stops accepting new work. You're not out of CPU or memory; you're just out of "slots" to do work.
Let's look at a concrete example. Imagine you're building a dashboard that needs to fetch data from five different microservices—billing, user profile, notifications, preferences, and activity logs—before returning a single response to the user. The most intuitive way to do this is to fire off five concurrent requests.
The Wall of Platform Threads
// The "classic" way we've done this for a decade
try (var executor = Executors.newFixedThreadPool(100)) {
List<Callable<String>> tasks = List.of(
() -> fetchBilling(),
() -> fetchProfile(),
() -> fetchNotifications(),
() -> fetchPreferences(),
() -> fetchLogs()
);
executor.invokeAll(tasks);
}
This looks fine on the surface, but here is where we hit the wall. Each thread in that pool is a "platform thread," which is essentially a thin wrapper around an OS thread. OS threads are heavy. They each reserve a significant amount of memory for their stack (often 1MB). If you try to scale this to 10,000 concurrent users, each needing 5 threads, you're suddenly asking the OS for 50,000 threads. Your RAM will vanish, and your CPU will spend more time context-switching between threads than actually executing your business logic. I've seen entire clusters crash simply because someone tried to "solve" latency by increasing the thread pool size too aggressively.
Shifting the Burden to the JVM
This is exactly why Project Loom gave us Virtual Threads. A virtual thread is not a 1:1 mapping to an OS thread. Instead, the JVM manages a small pool of "carrier" threads (actual OS threads) and multiplexes thousands—or even millions—of virtual threads onto them. When a virtual thread hits a blocking operation, like HttpClient.send() or Thread.sleep(), the JVM "unmounts" it from the carrier thread and parks it. The carrier thread is then free to do other work. Once the I/O operation completes, the JVM simply mounts the virtual thread back onto any available carrier thread to finish the job.
Here is how we rewrite that same dashboard logic using the new model:
// The Loom way: lightweight and scalable
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Callable<String>> tasks = List.of(
() -> fetchBilling(),
() -> fetchProfile(),
() -> fetchNotifications(),
() -> fetchPreferences(),
() -> fetchLogs()
);
executor.invokeAll(tasks);
}
The code looks almost identical, but the underlying mechanics are fundamentally different. We aren't limiting ourselves to 100 threads anymore. We are creating a new virtual thread for every single task. Because these threads are stored in the heap rather than the OS stack, they cost bytes instead of megabytes. You can suddenly handle 100,000 concurrent requests on a modest machine without breaking a sweat.
Why You Should Stop Pooling
If you take one thing away from this, let it be this: stop pooling virtual threads. In the old world, pooling was a necessity because creating a thread was a heavy operation. In the new world, creating a virtual thread is as cheap as creating a POJO. Pooling them actually harms performance because it introduces synchronization overhead and limits the very concurrency you're trying to achieve.
Now, a word of caution. Virtual threads don't make your code run faster; they make your system handle more throughput. If your task is CPU-intensive (like calculating a massive prime number), virtual threads won't help you—you're still limited by your CPU cores. They are a silver bullet for I/O-bound work, not computation-bound work. Also, be careful with synchronized blocks. If you hold a lock on a monitor while performing a blocking I/O operation, you might "pin" the virtual thread to the carrier thread, preventing other virtual threads from using that carrier. In those cases, you'll want to swap synchronized for ReentrantLock.
📋 Practical Task
Build a High-Concurrency Link Checker
Your goal is to build a utility that checks the status of 1,000 different URLs simultaneously to identify broken links. Using a standard FixedThreadPool, this would either be too slow (if the pool is small) or crash your system (if the pool is too large).
Requirements:
- Create a list of 1,000 mock URLs (you can just use strings).
- Implement a method
checkLink(String url)that simulates a network delay usingThread.sleep(Random.nextInt(100, 500))and returns whether the link is "Up" or "Down". - Use
Executors.newVirtualThreadPerTaskExecutor()to process all 1,000 links concurrently. - Measure the total time taken to complete all checks.
- Print the total count of "Up" vs "Down" links.
Challenge: Try running the same logic with a FixedThreadPool` of 10 threads, then 100 threads, and finally with Virtual Threads. Note the difference in total execution time and the system resources used.
There are no comments for now.