10 · Executor Framework & Thread Pools

Level: Intermediate

Pre-reading: 04 · Thread Creation & Lifecycle


Why Thread Pools?

Problem with Uncontrolled Threading

// ❌ Creates too many threads
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket client = serverSocket.accept();
    // Create new thread for each request (bad!)
    new Thread(() -> handleClient(client)).start();
}

// With 10,000 concurrent requests:
// 10,000 threads × 1MB = ~10GB RAM
// Context switching overhead skyrockets

Solution: Thread Pool

// ✅ Reuse threads
ExecutorService executor = Executors.newFixedThreadPool(100);  // Max 100 threads
ServerSocket serverSocket = new ServerSocket(8080);

while (true) {
    Socket client = serverSocket.accept();
    executor.submit(() -> handleClient(client));  // Reuse threads
}

ExecutorService Types

Fixed Thread Pool

ExecutorService executor = Executors.newFixedThreadPool(10);

// Submit tasks
for (int i = 0; i < 100; i++) {
    executor.submit(() -> doWork());  // Tasks queue up if all threads busy
}

executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);

Behavior:

  • Maintains exactly 10 threads
  • Tasks queue if all busy
  • Reuses threads (efficient)

Cached Thread Pool

ExecutorService executor = Executors.newCachedThreadPool();

// Creates new thread for each task initially
// Reuses idle threads
// Threads timeout if unused (default 60 seconds)

executor.submit(() -> fastTask());        // Reuses existing thread
executor.submit(() -> anotherFastTask()); // Reuses existing thread

// After 60s of no activity, threads are removed

Behavior:

  • Grows unbounded (dangerous!)
  • Good for short-lived tasks

Single Thread Executor

ExecutorService executor = Executors.newSingleThreadExecutor();

// All tasks execute sequentially in single thread
// Guarantees ordering
executor.submit(() -> task1());
executor.submit(() -> task2());  // Waits for task1
executor.submit(() -> task3());  // Waits for task2

Scheduled Executor

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);

// Delay execution
scheduler.schedule(() -> {
    System.out.println("Run after 5 seconds");
}, 5, TimeUnit.SECONDS);

// Repeat periodically
scheduler.scheduleAtFixedRate(() -> {
    System.out.println("Run every 10 seconds");
}, 0, 10, TimeUnit.SECONDS);

// Repeat with delay between runs
scheduler.scheduleWithFixedDelay(() -> {
    System.out.println("Run with 5 second delay");
}, 0, 5, TimeUnit.SECONDS);

Submitting Tasks

submit() vs execute()

ExecutorService executor = Executors.newFixedThreadPool(4);

// execute() - returns void
executor.execute(() -> System.out.println("Task"));

// submit() - returns Future
Future<?> future = executor.submit(() -> System.out.println("Task"));

// Check if done
System.out.println("Done: " + future.isDone());

// Wait for completion
future.get();  // Blocks until done

submit() with Callable

ExecutorService executor = Executors.newFixedThreadPool(4);

Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 42;
};

Future<Integer> future = executor.submit(task);

try {
    Integer result = future.get();
    System.out.println("Result: " + result);
} catch (ExecutionException e) {
    System.err.println("Task failed: " + e.getCause());
}

Shutting Down Executor

Graceful Shutdown

ExecutorService executor = Executors.newFixedThreadPool(10);

// Submit tasks...

executor.shutdown();  // Stop accepting new tasks

// Wait for in-flight tasks
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        System.err.println("Timeout waiting for tasks");
        executor.shutdownNow();  // Force shutdown
    }
} catch (InterruptedException e) {
    executor.shutdownNow();  // Force if interrupted
}

System.out.println("All tasks done");

Immediate Shutdown

ExecutorService executor = Executors.newFixedThreadPool(10);

// Submit tasks...

List<Runnable> pending = executor.shutdownNow();  // Stop immediately
System.out.println("Tasks cancelled: " + pending.size());

Future

Getting Results

ExecutorService executor = Executors.newFixedThreadPool(4);

Future<String> future = executor.submit(() -> {
    Thread.sleep(2000);
    return "Result";
});

// Non-blocking check
boolean done = future.isDone();
System.out.println("Done: " + done);

// Blocking get
String result = future.get();  // Waits indefinitely

// Get with timeout
try {
    String result = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    System.out.println("Task still running");
}

Cancellation

Future<Void> future = executor.submit(() -> {
    // Long running task
    for (int i = 0; i < 1000; i++) {
        if (Thread.currentThread().isInterrupted()) {
            return;
        }
        doWork();
    }
});

// Later, cancel task
boolean cancelled = future.cancel(true);  // true = interrupt if running
if (cancelled) {
    System.out.println("Task cancelled");
}

Key Takeaways

Pool Threads Use Case
Fixed Exact count General work, balanced load
Cached Unbounded growth Short-lived tasks
Single 1 Sequential execution
Scheduled N Periodic/delayed tasks

📚 Read the Original Blog Post

For more details and examples, read:


Why would I use executor instead of creating threads manually?

Automatic resource management, reuse, pooling, graceful shutdown, Future support.

What happens if I submit 1000 tasks to a fixed pool of 10 threads?

Tasks queue up. The 10 threads process them sequentially. Queue backed by LinkedBlockingQueue (unbounded by default).

Can I increase thread pool size dynamically?

Use ThreadPoolExecutor directly for configuration. ExecutorService via Executors factory methods have fixed sizes.