Executor Service & Thread Pools
Creating a new thread for every task is expensive. Each thread consumes ~1MB of stack memory and requires OS context switching. Instead of managing threads manually, Java provides the Executor Framework to reuse threads efficiently.
1. Why Use Thread Pools?
A Thread Pool is a collection of pre-started threads that are ready to execute tasks.
- Reuse: Threads are returned to the pool after finishing a task, saving creation costs.
- Throttling: Limits the number of active threads to prevent crashing the CPU.
- Management: Provides lifecycle methods (
shutdown,awaitTermination).
2. Types of Thread Pools
The Executors factory class provides several pre-configured pools:
| Pool Type | Description | Use Case |
|---|---|---|
| FixedThreadPool(n) | Reuse a fixed number of threads. If all are busy, tasks wait in a queue. | Predictable load, stable servers. |
| CachedThreadPool | Creates new threads as needed, but reuses idle ones. Threads die after 60s of inactivity. | Short-lived async tasks. |
| SingleThreadExecutor | Uses a single worker thread to execute tasks sequentially. | Ensuring order (like an event loop). |
| ScheduledThreadPool | Executes tasks after a delay or periodically. | Background maintenance, cron jobs. |
// Create a pool with 4 threads
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Executing Task " + taskId + " on " + Thread.currentThread().getName());
});
}
// Always shutdown your pool!
executor.shutdown();
Interactive: Thread Pool Visualizer
Watch how tasks queue up when the worker threads are busy.
3. Handling Results with Callable and Future
Standard Runnable tasks cannot return a result or throw checked exceptions. Use Callable<T> instead.
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(task);
// Do other work here...
try {
// Blocks until the result is ready
Integer result = future.get();
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
4. Go Implementation: Worker Pools
Go does not have a built-in “Thread Pool” class because goroutines are cheap. However, to throttle concurrency (e.g., limit DB connections), we use the Worker Pool Pattern with channels.
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
time.Sleep(time.Second) // Simulate work
fmt.Printf("Worker %d finished job %d\n", id, j)
results <- j * 2
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// Start 3 workers (Fixed Pool equivalent)
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Send jobs
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// Wait for workers in background
go func() {
wg.Wait()
close(results)
}()
// Collect results
for r := range results {
fmt.Println("Result:", r)
}
}
5. Best Practices
- Always Shutdown: Executor services keep the JVM running. Use
executor.shutdown()ortry-with-resources(Java 19+). - Avoid Unbounded Queues:
newFixedThreadPooluses an unbounded queue. If tasks come in faster than they are processed, you will get anOutOfMemoryError. - Use
virtualThreadPerTaskExecutor: In Java 21+, if your tasks are blocking (I/O), use virtual threads instead of pooling platform threads.
[!TIP] Virtual Threads vs Pools:
- Platform Threads (Old): Must be pooled because they are expensive (1MB RAM).
- Virtual Threads (New): Should NOT be pooled. Create a new virtual thread for every task.