What is a Thread Pool?
Imagine a restaurant with a kitchen staff. You could hire a new chef for every single order that comes in, then fire them when done (wasteful!). Or you could have a team of chefs ready to cook whenever orders arrive (efficient!). That's what a thread pool does - it maintains a group of reusable worker threads.
The Problem with Creating Threads Manually
// BAD: Creating 1000 threads - expensive and wasteful!
for (int i = 0; i < 1000; i++) {
Thread t = new Thread(() -> processTask());
t.start(); // Creates a new thread EVERY time
}
Problems:
❌ Creating threads is expensive (time + memory)
❌ 1000 threads running at once = system overload
❌ Thread creation/destruction overhead
❌ Hard to manage and control
The Solution: Thread Pool
// GOOD: Reuse a pool of 10 threads for 1000 tasks
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> processTask()); // Reuses existing threads!
}
executor.shutdown();
Benefits:
✅ Only 10 threads created
✅ Threads are reused for multiple tasks
✅ Controlled resource usage
✅ Better performance
How Thread Pools Work
┌─────────────────────────────┐
│ Task Queue │ ← Tasks waiting to be executed
│ [Task1][Task2][Task3]... │
└─────────────────────────────┘
↓ ↓ ↓
┌──────┐ ┌──────┐ ┌──────┐
│Thread│ │Thread│ │Thread│ ← Worker threads (reusable)
│ 1 │ │ 2 │ │ 3 │
└──────┘ └──────┘ └──────┘
1. Submit tasks to the pool
2. Tasks wait in queue
3. Available thread picks up task from queue
4. Thread executes task
5. Thread goes back to pool (ready for next task)
ExecutorService - The Thread Pool Interface
ExecutorService is Java's high-level API for managing thread pools. Think of it as a professional task manager for your threads.
Creating Thread Pools - Different Types
1. Fixed Thread Pool
// Creates a pool with exactly N threads
ExecutorService executor = Executors.newFixedThreadPool(5);
Use when: You know how many threads you need
Example: Web server with 100 worker threads
2. Cached Thread Pool
// Creates threads as needed, reuses idle threads
ExecutorService executor = Executors.newCachedThreadPool();
Use when: Many short-lived tasks
Example: Processing quick API calls
Warning: Can create too many threads if tasks pile up!
3. Single Thread Executor
// Only ONE thread executes tasks sequentially
ExecutorService executor = Executors.newSingleThreadExecutor();
Use when: Tasks must execute in order
Example: Writing to a log file (one at a time)
4. Scheduled Thread Pool
// Schedule tasks to run after delay or periodically
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
Use when: Need scheduled/delayed execution
Example: Backup database every hour
Complete Example - Fixed Thread Pool
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// Create pool with 3 worker threads
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit 10 tasks to the pool
for (int i = 1; i <= 10; i++) {
int taskNumber = i;
executor.submit(() -> {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": Executing Task " + taskNumber);
try {
Thread.sleep(2000); // Simulate work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + ": Completed Task " + taskNumber);
});
}
// Shutdown the pool
executor.shutdown(); // No new tasks accepted, existing tasks complete
try {
// Wait for all tasks to finish (max 1 minute)
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // Force shutdown
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
System.out.println("All tasks completed!");
}
}
Output:
pool-1-thread-1: Executing Task 1
pool-1-thread-2: Executing Task 2
pool-1-thread-3: Executing Task 3
... 2 seconds ...
pool-1-thread-1: Completed Task 1
pool-1-thread-2: Completed Task 2
pool-1-thread-3: Completed Task 3
pool-1-thread-1: Executing Task 4 ← Thread 1 reused!
pool-1-thread-2: Executing Task 5
pool-1-thread-3: Executing Task 6
... continues until all 10 tasks done ...
All tasks completed!
Notice: Only 3 threads handle all 10 tasks! Threads are reused efficiently.
submit() vs execute() - What's the Difference?
execute() - Fire and Forget
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.execute(() -> {
System.out.println("Task executed");
});
// Returns void - no way to get result or check status
// Use when: You don't need the result
submit() - Get a Future
ExecutorService executor = Executors.newFixedThreadPool(5);
Future<Integer> future = executor.submit(() -> {
Thread.sleep(1000);
return 42; // Return a result
});
// Returns Future - can get result later
Integer result = future.get(); // Blocks until task completes
System.out.println("Result: " + result); // 42
// Use when: You need the result or want to check status
Working with Future - Getting Results
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit a task that returns a result
Future<String> future = executor.submit(() -> {
System.out.println("Task started");
Thread.sleep(3000); // Simulate long operation
return "Task completed successfully!";
});
System.out.println("Main thread continues...");
// Check if task is done
if (!future.isDone()) {
System.out.println("Task is still running...");
}
// Get result - BLOCKS until task completes
String result = future.get(); // Waits for completion
System.out.println("Result: " + result);
// Get with timeout - don't wait forever!
try {
String result2 = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("Task took too long!");
future.cancel(true); // Cancel the task
}
executor.shutdown();
}
}
Future Methods
get()- Wait for task to complete and get resultget(timeout, unit)- Wait with timeoutisDone()- Check if task completedcancel()- Cancel the taskisCancelled()- Check if task was cancelled
Scheduled Executor - Running Tasks Periodically
Schedule Once (After Delay)
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// Run task after 5 seconds delay
scheduler.schedule(() -> {
System.out.println("Task executed after 5 seconds");
}, 5, TimeUnit.SECONDS);
scheduler.shutdown();
Schedule Repeatedly (Fixed Rate)
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// Run task every 10 seconds, starting after 0 second delay
scheduler.scheduleAtFixedRate(() -> {
System.out.println("Heartbeat: " + new Date());
}, 0, 10, TimeUnit.SECONDS);
// Runs every 10 seconds regardless of task duration
// Time between task START times = 10 seconds
Schedule with Fixed Delay
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// Wait 5 seconds AFTER task completes, then run again
scheduler.scheduleWithFixedDelay(() -> {
System.out.println("Processing batch...");
Thread.sleep(2000); // Task takes 2 seconds
}, 0, 5, TimeUnit.SECONDS);
// Time between task END and next START = 5 seconds
// Total cycle: 2s (task) + 5s (delay) = 7 seconds
Practical Example - Database Backup
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// Backup database every hour
scheduler.scheduleAtFixedRate(() -> {
System.out.println("Starting database backup...");
backupDatabase();
System.out.println("Backup completed!");
}, 0, 1, TimeUnit.HOURS);
Shutdown Methods - Stopping Executors
1. shutdown() - Graceful Shutdown
executor.shutdown();
// ✅ Existing tasks complete
// ❌ New tasks rejected
// Waits for tasks to finish naturally
2. shutdownNow() - Immediate Shutdown
List<Runnable> notExecuted = executor.shutdownNow();
// ❌ Running tasks interrupted
// ❌ Waiting tasks not started
// Returns list of tasks that never ran
3. awaitTermination() - Wait for Completion
executor.shutdown();
try {
// Wait up to 60 seconds for tasks to complete
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // Force shutdown after timeout
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
Best Practice - Proper Shutdown Pattern
ExecutorService executor = Executors.newFixedThreadPool(5);
try {
// Submit your tasks
executor.submit(() -> doWork());
} finally {
// Always shutdown in finally block!
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
Practical Example - Web Scraper
import java.util.concurrent.*;
import java.util.*;
public class WebScraper {
public static void main(String[] args) throws Exception {
List<String> urls = Arrays.asList(
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
"https://example.com/page4",
"https://example.com/page5"
);
// Pool of 3 threads to scrape 5 URLs
ExecutorService executor = Executors.newFixedThreadPool(3);
List<Future<String>> futures = new ArrayList<>();
// Submit all scraping tasks
for (String url : urls) {
Future<String> future = executor.submit(() -> scrapeWebsite(url));
futures.add(future);
}
// Collect all results
for (int i = 0; i < futures.size(); i++) {
String content = futures.get(i).get(); // Wait for result
System.out.println("Scraped: " + urls.get(i) + " - Length: " + content.length());
}
executor.shutdown();
System.out.println("All pages scraped!");
}
private static String scrapeWebsite(String url) {
System.out.println("Scraping: " + url);
try {
Thread.sleep(2000); // Simulate network delay
} catch (InterruptedException e) {}
return "Content from " + url; // Simulated content
}
}
Best Practices
Thread Pool Best Practices
- ✅ Always shutdown executors: Use try-finally to ensure cleanup
- ✅ Size pools appropriately: CPU-bound = cores, I/O-bound = more threads
- ✅ Use submit() over execute(): Better error handling with Future
- ✅ Handle InterruptedException: Don't ignore it!
- ✅ Use timeouts with get(): Prevent infinite waiting
- ✅ Name thread pools: Easier debugging
- ✅ Monitor pool health: Check queue sizes, rejected tasks
- ❌ Don't create unlimited pools: CachedThreadPool can be dangerous
- ❌ Don't forget to shutdown: Threads keep JVM alive
- ❌ Don't use Thread directly: Use ExecutorService instead
Choosing Pool Size
// CPU-intensive tasks (calculations, data processing)
int cpuCores = Runtime.getRuntime().availableProcessors();
ExecutorService cpuPool = Executors.newFixedThreadPool(cpuCores);
// I/O-intensive tasks (network, file operations)
ExecutorService ioPool = Executors.newFixedThreadPool(cpuCores * 2);
Why It Matters
- Performance: Reusing threads is much faster than creating new ones
- Resource Management: Prevents system overload from too many threads
- Scalability: Handle thousands of tasks with limited resources
- Simplicity: Easier to manage than manual thread creation
- Professional Standard: Industry best practice for concurrent programming
Remember
Thread pools are like having a skilled team ready to work, rather than hiring and training someone new for every single task. Much more efficient!