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
84: The Executor Framework
I've seen this pattern in countless code reviews over the years. A developer needs to do something in the background—maybe send an email or process a file—and their first instinct is to just wrap the logic in a new Thread() and call .start(). It seems intuitive. It works fine during local testing with two or three items. Then, it hits production, the load spikes, and the entire JVM crashes with an OutOfMemoryError: unable to create new native thread.
The "New Thread Per Task" Trap
Take a look at this snippet. Imagine we're building a system that processes a batch of log files from a directory to find error patterns.
public class LogAnalyzer {
public void processLogs(List<File> logFiles) {
for (File file : logFiles) {
// I'll just spin up a thread for each file to make it fast!
new Thread(() -> {
analyzeFile(file);
}).start();
}
}
private void analyzeFile(File file) {
// Simulate heavy I/O and processing
System.out.println("Analyzing " + file.getName() + " on " + Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (InterruptedException e) { }
}
}
On the surface, this looks "concurrent." But here is the problem: if logFiles contains 5,000 files, you just told the OS to create 5,000 platform threads. Each thread in Java has its own stack memory (usually 1MB by default). You've just attempted to allocate 5GB of RAM just for the thread stacks, not even counting the actual data processing. Even if you have the RAM, the CPU will spend more time "context switching"—swapping between these thousands of threads—than actually doing the work.
Managing Resources with a Fixed Thread Pool
Instead of manually managing the lifecycle of every single thread, we use the Executor Framework. The core idea is to decouple task submission from task execution. You define a pool of worker threads that stay alive and pull tasks from a queue.
Here is how I would rewrite that LogAnalyzer to be production-ready:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class LogAnalyzer {
public void processLogs(List<File> logFiles) {
// Create a pool with a fixed number of threads (e.g., based on CPU cores)
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(cores);
for (File file : logFiles) {
// We submit a Runnable to the executor instead of creating a new Thread
executor.submit(() -> {
analyzeFile(file);
});
}
// Crucial: Tell the executor to stop accepting new tasks and shut down
executor.shutdown();
try {
// Wait for existing tasks to finish so the program doesn't exit early
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
private void analyzeFile(File file) {
System.out.println("Analyzing " + file.getName() + " on " + Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (InterruptedException e) { }
}
}
Now, no matter if you have 10 files or 10 million, you will only ever have a handful of threads running (equal to your CPU core count). The ExecutorService manages an internal LinkedBlockingQueue; tasks wait there patiently until a worker thread becomes available. This keeps your memory usage predictable and your CPU efficient.
The Danger of the "Forgotten" Shutdown
One thing that trips up a lot of developers is forgetting to call shutdown(). Unlike a local variable that gets garbage collected, the threads in an ExecutorService are often non-daemon threads. If you don't shut down the executor, the JVM will keep running even after your main method finishes because those worker threads are still idling, waiting for more work that will never come.
I generally recommend the shutdown() followed by awaitTermination() pattern. shutdown() is a polite request: "Finish what you're doing, but don't take any new orders." shutdownNow() is the emergency brake: "Stop everything immediately and tell me what tasks didn't get finished."
📋 Practical Task
Exercise: Building a Parallel Image Metadata Extractor
You are tasked with building a tool that simulates extracting metadata from a large folder of images. Since reading file headers is an I/O-bound task, you want to process multiple images concurrently without crashing the system.
Requirements:
- Create a class
ImageProcessorwith a methodprocessImages(List<String> imageNames). - Use a
FixedThreadPoolwith exactly 4 threads. - For each image name in the list, submit a task that prints:
"Processing [imageName] on [threadName]". - Simulate a delay of 200ms per image using
Thread.sleep()to mimic disk I/O. - Ensure the
ExecutorServiceis shut down correctly and the main thread waits for all tasks to complete before printing"All images processed." - Test your code with a list of at least 20 image filenames to verify that only 4 unique thread names are ever used in the console output.
There are no comments for now.