The Executor Framework
The Executor Framework
Before Java 5, running something concurrently meant manually creating a Thread, starting it, and hoping for the best. That approach has two fundamental problems: thread creation is expensive (each thread costs roughly 1 MB of stack memory and hundreds of microseconds of OS time), and there is no built-in mechanism for reuse, error reporting, or lifecycle management. The Executor framework — introduced in java.util.concurrent in Java 5 — solves all of this by separating the what (a unit of work) from the how (the thread strategy that runs it).
The Core Abstraction: Executor
The entire framework rests on a single interface:
That one method is the whole contract. Any object that accepts a Runnable and eventually runs it is an Executor. The simplest possible implementation runs the task directly on the calling thread:
A more useful one queues the task on a new thread:
Both are valid Executor implementations. The calling code does not change — only the strategy does. That is the power of the abstraction.
ExecutorService: Lifecycle Management
ExecutorService extends Executor with two important additions: the ability to submit tasks that return results, and the ability to shut down the executor in an orderly way.
shutdown(). A stray ExecutorService is a common cause of applications that hang instead of terminating cleanly.
Thread Pools: Why They Exist
A thread pool is an ExecutorService that maintains a fixed or dynamic set of worker threads. When you submit a task, the pool hands it to an idle thread (or queues it if all threads are busy). When the task finishes, the thread returns to the pool and waits for the next task — no teardown, no recreation.
The benefits are significant:
- Reduced latency — threads are pre-created; submitting a task has near-zero overhead.
- Resource control — you choose how many threads can run simultaneously, preventing thread explosion under load.
- Reuse — the same thread objects handle thousands of tasks over their lifetime.
Creating Thread Pools with Executors
The Executors factory class provides the most common pool types. Here are the ones you will use most often:
A minimal end-to-end example: submit ten tasks to a fixed pool, then shut it down and wait for completion.
Run this and you will see three thread names cycling across ten tasks — proof that the threads are being reused.
Choosing the Right Pool Type
- CPU-bound work — use
newFixedThreadPool(Runtime.getRuntime().availableProcessors()). More threads than cores just adds context-switch overhead without extra throughput. - IO-bound / blocking work — threads spend most of their time waiting (network, disk). A larger pool (or a cached pool) lets the CPU stay busy while others wait. As of Java 21 you can also use virtual threads, which are dramatically cheaper for blocking IO.
- Unbounded work with bursty traffic —
newCachedThreadPool()is convenient but dangerous under sustained load: if tasks arrive faster than they complete, the pool spawns unbounded threads and you run out of memory. Prefer a customThreadPoolExecutorwith a bounded queue in production. - Sequential guarantee —
newSingleThreadExecutor()gives you a serial, ordered execution channel without any explicit synchronization.
ThreadPoolExecutor exposes all of those knobs. You will explore it in detail in the next lesson on pool types.
Proper Shutdown Pattern
A robust shutdown follows the two-phase pattern recommended in the Java documentation:
shutdownNow() sends an interrupt to all running threads, which only works if those threads actually check their interrupted status (e.g. blocking on IO or calling Thread.sleep()). Tasks that ignore interrupts will keep running regardless.
Summary
The Executor framework replaces manual thread management with a clean abstraction. Executor decouples task submission from execution strategy. ExecutorService adds lifecycle management and result-bearing task submission. Thread pools pre-create and reuse threads to reduce latency and cap resource consumption. The factory methods in Executors cover the most common use cases; choose a pool type based on whether your tasks are CPU-bound or IO-bound. Always shut down your executor services to prevent JVM hang.