Thread Pools & ExecutorService

Efficient Thread Management

← Back to Index

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

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

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!