The ExecutorService interface builds on the Executor interface to manage thread pools and execute tasks asynchronously. Java provides several implementations via the Executors utility class, each suited for different use cases. Lets understand with examples, focusing on their purpose and behavior.
Hierarchy:
java.util.concurrent
├── Executor (Interface)
│ └── ExecutorService (Interface)
│ ├── AbstractExecutorService (Abstract Class)
│ │ ├── ThreadPoolExecutor (Class)
│ │ │ └── ScheduledThreadPoolExecutor (Class) [also implements ScheduledExecutorService]
│ │ └── ForkJoinPool (Class)
│ └── ScheduledExecutorService (Interface)
│ └── ScheduledThreadPoolExecutor (Class)
What is ExecutorService?
- Definition: A subinterface of Executor that adds lifecycle management (e.g., shutdown) and task submission with results (e.g., Future).
- Key Methods: execute(Runnable), submit(Callable), shutdown(), invokeAll().
- Purpose: Simplifies thread management by pooling threads and queuing tasks.
Types of Executor Services (via Executors Factory Methods)
The Executors class provides factory methods to create different ExecutorService implementations. Here are the main types:
1. newFixedThreadPool(int nThreads)
- What It Is: Creates a thread pool with a fixed number of threads. Excess tasks are queued until a thread becomes available.
- Behavior:
- Maintains nThreads reusable threads.
- Tasks wait in an unbounded queue (LinkedBlockingQueue) if all threads are busy.
- Use Case: When you want to limit concurrency (e.g., server handling a fixed number of requests).
import java.util.concurrent.*; public class FixedThreadPoolExample { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); // 2 threads for (int i = 0; i < 5; i++) { executor.submit(() -> { System.out.println(Thread.currentThread().getName() + " executing"); try { Thread.sleep(1000); } catch (Exception e) {} }); } executor.shutdown(); } }
Output (approximate):
pool-1-thread-1 executing pool-1-thread-2 executing (1s pause) pool-1-thread-1 executing pool-1-thread-2 executing (1s pause) pool-1-thread-1 executing
Note: Only 2 tasks run at a time; others queue.
2. newCachedThreadPool()
What It Is: Creates a thread pool that grows and shrinks dynamically based on demand.
Behavior:
Creates new threads as needed (up to Integer.MAX_VALUE).
Idle threads are terminated after 60 seconds (configurable).
Use Case: Short-lived, unpredictable tasks (e.g., handling bursty web requests).
import java.util.concurrent.*; public class CachedThreadPoolExample { public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { executor.submit(() -> { System.out.println(Thread.currentThread().getName() + " executing"); try { Thread.sleep(500); } catch (Exception e) {} }); } executor.shutdown(); } }
Output (approximate):
pool-1-thread-1 executing pool-1-thread-2 executing pool-1-thread-3 executing pool-1-thread-4 executing pool-1-thread-5 executing
Note: All 5 tasks may run concurrently; threads are reused or created as needed.
3. newSingleThreadExecutor()
- What It Is: Creates a single-threaded executor that processes tasks sequentially.
- Behavior:
- Uses one thread with an unbounded queue (LinkedBlockingQueue).
- Guarantees tasks execute one at a time in submission order.
- Use Case: Sequential task execution (e.g., logging, background jobs).
import java.util.concurrent.*; public class SingleThreadExecutorExample { public static void main(String[] args) { ExecutorService executor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 3; i++) { executor.submit(() -> { System.out.println(Thread.currentThread().getName() + " executing"); try { Thread.sleep(1000); } catch (Exception e) {} }); } executor.shutdown(); } }
- Output:
pool-1-thread-1 executing (1s pause) pool-1-thread-1 executing (1s pause) pool-1-thread-1 executing</code>
- Note: One thread handles all tasks sequentially.
4. newScheduledThreadPool(int corePoolSize)
- What It Is: Creates a thread pool that can schedule tasks to run after a delay or periodically.
- Behavior:
- Returns a ScheduledExecutorService (subinterface of ExecutorService).
- Supports schedule(), scheduleAtFixedRate(), scheduleWithFixedDelay().
- Use Case: Timed tasks (e.g., cron-like jobs, periodic updates).
import java.util.concurrent.*; public class ScheduledThreadPoolExample { public static void main(String[] args) { ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); // Schedule a task with 2s delay executor.schedule(() -> { System.out.println("Task executed after delay"); }, 2, TimeUnit.SECONDS); // Schedule a task every 1s after initial 0s delay executor.scheduleAtFixedRate(() -> { System.out.println("Periodic task: " + System.currentTimeMillis()); }, 0, 1, TimeUnit.SECONDS); // Let it run briefly, then stop try { Thread.sleep(5000); } catch (Exception e) {} executor.shutdown(); } }
- Output (approximate):
Periodic task: 1698765432100 Periodic task: 1698765433100 Task executed after delay Periodic task: 1698765434100 ...
- Note: Combines fixed pool with scheduling capabilities.
5. newWorkStealingPool() (Java 8+)
- What It Is: Creates a work-stealing thread pool optimized for parallel tasks (uses ForkJoinPool).
- Behavior:
- Default parallelism = number of CPU cores (Runtime.getRuntime().availableProcessors()).
- Threads “steal” tasks from others’ queues to balance load.
- Use Case: CPU-intensive parallel tasks (e.g., divide-and-conquer algorithms).
import java.util.concurrent.*; public class WorkStealingPoolExample { public static void main(String[] args) { ExecutorService executor = Executors.newWorkStealingPool(); for (int i = 0; i < 4; i++) { executor.submit(() -> { System.out.println(Thread.currentThread().getName() + " executing"); try { Thread.sleep(1000); } catch (Exception e) {} }); } executor.shutdown(); } }
ForkJoinPool-1-worker-1 executing ForkJoinPool-1-worker-2 executing ForkJoinPool-1-worker-3 executing ForkJoinPool-1-worker-4 executing
- Note: Efficient for parallel workloads; thread count adapts to CPU.
Comparison Table
Type | Thread Count | Task Queue | Use Case |
---|---|---|---|
FixedThreadPool | Fixed (n) | Unbounded | Limited concurrency |
CachedThreadPool | Dynamic (0 to max) | None (creates threads) | Short, bursty tasks |
SingleThreadExecutor | 1 | Unbounded | Sequential tasks |
ScheduledThreadPool | Fixed (n) | Delayed/Scheduled | Timed/periodic tasks |
WorkStealingPool | CPU cores (default) | Work-stealing | Parallel, CPU-bound tasks |
Key Notes
- Customization: Beyond Executors, use ThreadPoolExecutor directly for fine-tuned control (e.g., custom queue size, rejection policy).
- Shutdown: Always call shutdown() or shutdownNow() to avoid resource leaks.
- Callable vs. Runnable: submit() supports Callable (returns Future), while execute() takes Runnable (no return).
Conclusion
- FixedThreadPool: Controlled concurrency.
- CachedThreadPool: Flexible, bursty workloads.
- SingleThreadExecutor: One-at-a-time tasks.
- ScheduledThreadPool: Timing is everything.
- WorkStealingPool: Parallel power.