Concurrent Utilities

The Executor Framework

15 min Lesson 1 of 13

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:

public interface Executor { void execute(Runnable command); }

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:

Executor directExecutor = Runnable::run; directExecutor.execute(() -> System.out.println("Runs on caller thread"));

A more useful one queues the task on a new thread:

Executor threadPerTask = task -> new Thread(task).start(); threadPerTask.execute(() -> System.out.println("Runs 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.

public interface ExecutorService extends Executor { <T> Future<T> submit(Callable<T> task); Future<?> submit(Runnable task); void shutdown(); // no new tasks; finish queued work List<Runnable> shutdownNow(); // interrupt running tasks; return unstarted ones boolean isTerminated(); boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; }
Always shut down an ExecutorService. Its threads are non-daemon by default, which means the JVM will not exit if you forget to call 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:

import java.util.concurrent.*; // 1. Fixed thread pool — exactly N threads, tasks queue when all are busy ExecutorService fixed = Executors.newFixedThreadPool(4); // 2. Cached thread pool — creates threads on demand, reuses idle ones, // terminates threads idle for 60 s ExecutorService cached = Executors.newCachedThreadPool(); // 3. Single-threaded executor — one thread, tasks run in submission order ExecutorService single = Executors.newSingleThreadExecutor(); // 4. Scheduled executor — run tasks after a delay or on a fixed schedule ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);

A minimal end-to-end example: submit ten tasks to a fixed pool, then shut it down and wait for completion.

import java.util.concurrent.*; public class FixedPoolDemo { public static void main(String[] args) throws InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(3); for (int i = 1; i <= 10; i++) { int taskId = i; pool.execute(() -> { System.out.printf("Task %d running on %s%n", taskId, Thread.currentThread().getName()); }); } pool.shutdown(); // stop accepting new tasks pool.awaitTermination(10, TimeUnit.SECONDS); // wait for current tasks System.out.println("All tasks done."); } }

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 trafficnewCachedThreadPool() 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 custom ThreadPoolExecutor with a bounded queue in production.
  • Sequential guaranteenewSingleThreadExecutor() gives you a serial, ordered execution channel without any explicit synchronization.
Prefer explicit ThreadPoolExecutor in production. The factory methods are fine for learning and simple tools, but in a real service you want to name your threads (easier debugging), bound your queue depth, and define a rejection policy. 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:

static void shutdownAndAwait(ExecutorService pool) { pool.shutdown(); // phase 1: reject new tasks, let queued tasks finish try { if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { pool.shutdownNow(); // phase 2: interrupt running tasks if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { System.err.println("Pool did not terminate"); } } } catch (InterruptedException ex) { pool.shutdownNow(); Thread.currentThread().interrupt(); // restore interrupted status } }
Do not call shutdownNow() directly unless you need to cancel work in progress. 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.