☕ Java

Executors Framework

The Executors framework, introduced in Java 5 as part of java.util.concurrent, provides a high-level abstraction layer over raw thread management. Before it existed, Java developers had to create, start, join, and destroy Thread objects manually — coupling task definition to thread lifecycle management and making resource control, error handling, and shutdown extremely error-prone. The framework separates the task (what to run) from the executor (how and when to run it), allowing application code to submit units of work without knowing whether they will execute in a thread pool, a single background thread, the calling thread, or some other mechanism entirely. The three core interfaces — Executor, ExecutorService, and ScheduledExecutorService — define a clean hierarchy of capabilities, and the Executors factory class provides pre-built implementations covering the most common thread pool configurations. This entry covers the full interface hierarchy and the contract of each method, all Executors factory methods and the precise thread pool configuration each produces, executor lifecycle and the shutdown protocol, rejection policies and when they fire, ScheduledExecutorService for delayed and periodic task execution, and the ThreadFactory interface for customizing thread creation.

The Interface Hierarchy — Executor, ExecutorService, ScheduledExecutorService

The Executors framework is built on three interfaces that form a capability hierarchy, each extending the previous with richer semantics. Executor is the root interface with a single method: execute(Runnable command). It decouples task submission from task execution mechanics — the caller submits a Runnable, and the Executor decides how to run it. The Executor contract does not specify whether execution is synchronous or asynchronous, in the calling thread or a new thread, immediate or deferred. It is the narrowest possible interface for task submission. An implementation that calls command.run() directly in the calling thread satisfies the Executor contract; so does a ThreadPoolExecutor that queues the task for a background thread. This flexibility makes Executor the correct parameter type when a method only needs to submit work and does not need to track results or manage lifecycle. ExecutorService extends Executor with lifecycle management and result-bearing task submission. submit(Callable<T>) and submit(Runnable) return Future objects that allow the caller to retrieve results, check completion, and cancel tasks. invokeAll(Collection<Callable<T>>) submits all tasks and returns a List<Future<T>> after all complete. invokeAny(Collection<Callable<T>>) submits all tasks and returns the result of the first to complete successfully, cancelling the rest. The lifecycle methods — shutdown(), shutdownNow(), isShutdown(), isTerminated(), awaitTermination() — allow ordered shutdown where no new tasks are accepted but submitted tasks complete, or immediate shutdown that interrupts running tasks. ExecutorService is the correct type for most application code that needs concurrent task execution with result retrieval. ScheduledExecutorService extends ExecutorService with time-based scheduling. schedule(Callable<V> callable, long delay, TimeUnit unit) executes a task once after the specified delay and returns a ScheduledFuture. scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) executes a task repeatedly with a fixed period between starts — if a task takes longer than the period, the next execution starts immediately after (no overlap; execution is not truly concurrent). scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) executes a task repeatedly with a fixed delay between the end of one execution and the start of the next — more appropriate when task duration is variable and overlap is to be avoided. All scheduled tasks return ScheduledFuture, which extends Future with getDelay() for inspecting remaining delay.
Java
// ── Executor — the minimal interface ────────────────────────────────
Executor direct = Runnable::run;   // simplest possible: runs in caller's thread
Executor async  = r -> new Thread(r).start();   // simplest async: new thread per task

direct.execute(() -> System.out.println("Direct: " + Thread.currentThread().getName()));
async.execute(()  -> System.out.println("Async:  " + Thread.currentThread().getName()));
// Direct: main    (same thread)
// Async:  Thread-0 (new thread)

// A method that needs only task submission, not lifecycle:
void runTask(Executor executor, Runnable task) {
    executor.execute(task);  // works with ThreadPoolExecutor, direct, async, or any Executor
}

// ── ExecutorService — lifecycle + result-bearing submission ───────────
ExecutorService service = Executors.newFixedThreadPool(4);

// submit Runnable — Future<Void> just for completion signaling:
Future<?> voidFuture = service.submit(() -> System.out.println("Runnable task"));
voidFuture.get();   // blocks until complete; returns null

// submit Callable — Future<T> for result retrieval:
Future<Integer> resultFuture = service.submit(() -> {
    Thread.sleep(100);
    return 42;
});
Integer result = resultFuture.get();   // blocks; returns 42
System.out.println("Result: " + result);

// invokeAll — submit batch, wait for ALL:
List<Callable<String>> tasks = List.of(
    () -> "Task A",
    () -> { Thread.sleep(50); return "Task B"; },
    () -> "Task C"
);
List<Future<String>> futures = service.invokeAll(tasks);  // blocks until all done
for (Future<String> f : futures) System.out.println(f.get());

// invokeAny — submit batch, return FIRST successful result, cancel rest:
String fastest = service.invokeAny(List.of(
    () -> { Thread.sleep(200); return "slow"; },
    () -> { Thread.sleep(10);  return "fast"; },
    () -> { Thread.sleep(100); return "medium"; }
));
System.out.println("Fastest: " + fastest);  // fast

// ── ScheduledExecutorService — time-based scheduling ─────────────────
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

// One-shot with delay:
ScheduledFuture<String> delayed = scheduler.schedule(
    () -> "Delayed result",
    500, TimeUnit.MILLISECONDS
);
System.out.println("Remaining delay: " + delayed.getDelay(TimeUnit.MILLISECONDS) + "ms");
System.out.println("Delayed: " + delayed.get());   // blocks ~500ms

// Fixed rate: starts at 0ms, then every 1000ms (regardless of task duration):
AtomicInteger fixedRateCount = new AtomicInteger(0);
ScheduledFuture<?> fixedRate = scheduler.scheduleAtFixedRate(() -> {
    System.out.println("Fixed rate tick " + fixedRateCount.incrementAndGet());
}, 0, 1000, TimeUnit.MILLISECONDS);

// Fixed delay: waits 500ms AFTER each task ends before starting the next:
ScheduledFuture<?> fixedDelay = scheduler.scheduleWithFixedDelay(() -> {
    System.out.println("Fixed delay tick");
    try { Thread.sleep(200); } catch (InterruptedException e) {}
    // Next run starts 500ms after this sleep completes: total gap = 200+500 = 700ms
}, 0, 500, TimeUnit.MILLISECONDS);

Thread.sleep(3500);
fixedRate.cancel(false);
fixedDelay.cancel(false);
scheduler.shutdown();

Executors Factory Methods, ThreadFactory, and Rejection Policies

The Executors utility class provides eight static factory methods that cover the most common executor configurations. Each factory wraps a ThreadPoolExecutor or ScheduledThreadPoolExecutor with specific parameters. Understanding the underlying configuration each factory produces is essential for choosing the right factory and for knowing when to construct a ThreadPoolExecutor directly with custom parameters. newFixedThreadPool(int nThreads) creates a pool with exactly nThreads threads, an unbounded LinkedBlockingQueue, and no idle thread timeout. Threads are created on demand up to nThreads and never terminated due to inactivity. The unbounded queue means tasks are never rejected — they accumulate in the queue indefinitely. This is both an advantage (no rejection under any load) and a danger (unbounded memory consumption if tasks are submitted faster than they complete). Use for CPU-bound workloads where nThreads is set to Runtime.getRuntime().availableProcessors(). newCachedThreadPool() creates a pool with zero core threads, Integer.MAX_VALUE maximum threads, a 60-second idle timeout, and a SynchronousQueue (which does not buffer — each submitted task must immediately be taken by a thread). If no idle thread is available when a task is submitted, a new thread is created. If a thread has been idle for 60 seconds, it terminates. This pool scales dynamically from zero to effectively unlimited threads. It is excellent for short-lived, bursty I/O-bound tasks where threads spend most of their time waiting. It is dangerous for CPU-bound tasks or sustained high load, where it will create as many threads as submitted tasks, potentially exhausting memory and scheduling overhead. newSingleThreadExecutor() creates a pool with exactly one thread and an unbounded queue. Tasks are executed sequentially in submission order. The thread is replaced automatically if it dies due to an uncaught exception. Use when sequential execution of tasks is required with no thread management overhead. newScheduledThreadPool(int corePoolSize) creates a ScheduledThreadPoolExecutor with the given number of core threads and an unbounded delayed work queue. It replaces the error-prone Timer class, which used a single thread for all scheduled tasks (one slow task blocks all others) and swallowed exceptions (one task throwing cancels all future tasks). ScheduledThreadPoolExecutor runs each task in a separate core pool thread, so one slow task doesn't block others. ThreadFactory is a single-method interface (Thread newThread(Runnable r)) that all ExecutorService implementations use to create threads. By default, Executors factories use DefaultThreadFactory, which creates non-daemon threads with NORM_PRIORITY and names like "pool-1-thread-1". Providing a custom ThreadFactory allows setting meaningful thread names, daemon status, priority, uncaught exception handlers, and thread groups. Custom ThreadFactory is one of the most impactful and commonly neglected improvements available to executor-based code. RejectedExecutionHandler is invoked when a task cannot be accepted because the pool is shut down or the queue and pool are both full. Four policies are provided: AbortPolicy (default — throws RejectedExecutionException), CallerRunsPolicy (executes the task in the caller's thread, providing backpressure), DiscardPolicy (silently drops the task), and DiscardOldestPolicy (drops the oldest queued task and retries submission). CallerRunsPolicy is frequently the right choice for load-shedding scenarios because it slows down producers when the pool is saturated without losing tasks.
Java
// ── All Executors factory methods ────────────────────────────────────
// Fixed: exact N threads, unbounded queue — good for CPU-bound work
ExecutorService fixed = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors()
);

// Cached: 0–∞ threads, 60s idle timeout — good for short-lived I/O tasks
ExecutorService cached = Executors.newCachedThreadPool();

// Single: 1 thread, unbounded queue — sequential execution guaranteed
ExecutorService single = Executors.newSingleThreadExecutor();

// Scheduled: N core threads, delayed/periodic execution
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);

// Variants with custom ThreadFactory (Java 8+):
ExecutorService fixedNamed = Executors.newFixedThreadPool(4, r -> {
    Thread t = new Thread(r);
    t.setName("worker-" + t.getId());
    t.setDaemon(true);
    return t;
});

// WorkStealing (Java 8+): parallelism = availableProcessors, uses ForkJoinPool:
ExecutorService workStealing = Executors.newWorkStealingPool();

// ── Custom ThreadFactory — always name your threads ───────────────────
class NamedThreadFactory implements ThreadFactory {
    private final String poolName;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final boolean daemon;

    NamedThreadFactory(String poolName, boolean daemon) {
        this.poolName = poolName;
        this.daemon = daemon;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, poolName + "-" + threadNumber.getAndIncrement());
        t.setDaemon(daemon);
        t.setPriority(Thread.NORM_PRIORITY);
        t.setUncaughtExceptionHandler((thread, ex) ->
            System.err.printf("[%s] Uncaught: %s%n", thread.getName(), ex.getMessage())
        );
        return t;
    }
}

ExecutorService namedPool = new ThreadPoolExecutor(
    4, 4, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>(1000),
    new NamedThreadFactory("order-processor", false),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

// Thread dump will show: "order-processor-1", "order-processor-2", etc.

// ── Rejection policies ────────────────────────────────────────────────
// Bounded queue — will fill up and trigger rejection:
ThreadPoolExecutor bounded = new ThreadPoolExecutor(
    2, 2, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(5)   // queue capacity = 5
);

// AbortPolicy (default): throws RejectedExecutionException
bounded.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
try {
    for (int i = 0; i < 100; i++) bounded.execute(() -> {
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
    });
} catch (RejectedExecutionException e) {
    System.out.println("Rejected: " + e.getMessage());
}

// CallerRunsPolicy: caller thread runs the task — natural backpressure
ThreadPoolExecutor withBackpressure = new ThreadPoolExecutor(
    2, 2, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(5),
    new ThreadPoolExecutor.CallerRunsPolicy()
);
// When queue is full: submit() blocks on caller until a slot opens
// This automatically slows down producers — no tasks lost, no exceptions

// DiscardPolicy: silently drops rejected tasks (dangerous — silent data loss)
bounded.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

// DiscardOldestPolicy: drops oldest queued task, retries submission
bounded.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());

// Custom policy: log and count rejections
bounded.setRejectedExecutionHandler((task, executor) -> {
    System.err.println("Task rejected: " + task + " — queue size: " + executor.getQueue().size());
    // metrics.increment("executor.rejections");
});

Executor Lifecycle, Shutdown Protocol, and Monitoring

ExecutorService defines a precise lifecycle with three phases: running (accepting and executing tasks), shutdown (not accepting new tasks but executing submitted ones), and terminated (all tasks complete, all threads stopped). Managing this lifecycle correctly is critical for clean application shutdown; incorrect shutdown is one of the most common sources of thread leaks and JVM hangs in production Java applications. shutdown() initiates an orderly shutdown: no new tasks are accepted (submit() and execute() throw RejectedExecutionException), but all previously submitted tasks — including those currently executing and those queued but not yet started — are allowed to complete. The calling thread is not blocked; shutdown() returns immediately. The pool transitions from RUNNING to SHUTDOWN state. shutdownNow() initiates an abrupt shutdown: it attempts to interrupt all currently executing tasks (by calling Thread.interrupt() on each worker thread), drains the task queue and returns the queued-but-not-started tasks as a List<Runnable>, and stops accepting new tasks. Tasks that ignore interruption will continue running. shutdownNow() is appropriate for error scenarios or time-critical shutdown where pending tasks are no longer relevant. awaitTermination(long timeout, TimeUnit unit) blocks the calling thread until all tasks have completed after shutdown, the timeout elapses, or the calling thread is interrupted. It returns true if the executor reached the TERMINATED state within the timeout, false if the timeout elapsed first. The canonical shutdown pattern combines shutdown() with awaitTermination() and a fallback to shutdownNow(): call shutdown(), then awaitTermination() for a grace period, then call shutdownNow() if tasks haven't completed, then awaitTermination() again with a shorter timeout to verify. This pattern is recommended in the ThreadPoolExecutor Javadoc and covers orderly, graceful, and forced shutdown in sequence. isShutdown() returns true once shutdown() or shutdownNow() has been called, regardless of whether tasks are still running. isTerminated() returns true only after shutdown() or shutdownNow() has been called AND all tasks have completed. The TERMINATED state also triggers all threads waiting in awaitTermination() to be released. ThreadPoolExecutor exposes a rich monitoring API beyond the lifecycle methods: getPoolSize() (current number of threads), getActiveCount() (threads currently executing tasks), getCompletedTaskCount() (total tasks finished since creation), getTaskCount() (total tasks submitted), getQueue() (the work queue, allowing inspection of queue depth). These metrics are invaluable for tuning and monitoring pool behavior in production, and should be exported to a metrics system periodically.
Java
// ── Canonical shutdown pattern ────────────────────────────────────────
ExecutorService pool = Executors.newFixedThreadPool(4);

// Submit some work:
for (int i = 0; i < 10; i++) {
    int taskId = i;
    pool.submit(() -> {
        try { Thread.sleep(200); } catch (InterruptedException e) {
            System.out.println("Task " + taskId + " interrupted");
            Thread.currentThread().interrupt();
        }
        System.out.println("Task " + taskId + " completed");
    });
}

// Shutdown protocol from ThreadPoolExecutor Javadoc:
void shutdownAndAwaitTermination(ExecutorService executor) {
    executor.shutdown();   // stop accepting; let queued tasks finish
    try {
        // Wait up to 60 seconds for tasks to complete:
        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
            executor.shutdownNow();   // force stop remaining tasks
            // Wait another 60 seconds for forced tasks to respond to interrupt:
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                System.err.println("Pool did not terminate");
            }
        }
    } catch (InterruptedException ex) {
        executor.shutdownNow();   // interrupted while waiting — force stop
        Thread.currentThread().interrupt();   // restore interrupt flag
    }
}

shutdownAndAwaitTermination(pool);
System.out.println("Pool terminated: " + pool.isTerminated());  // true

// ── State transitions ─────────────────────────────────────────────────
ExecutorService tracked = Executors.newFixedThreadPool(2);
System.out.println("Running:    isShutdown=" + tracked.isShutdown() +
                   " isTerminated=" + tracked.isTerminated());
// Running:    isShutdown=false isTerminated=false

tracked.shutdown();
System.out.println("Shutdown:   isShutdown=" + tracked.isShutdown() +
                   " isTerminated=" + tracked.isTerminated());
// Shutdown:   isShutdown=true  isTerminated=false (tasks may still run)

tracked.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("Terminated: isShutdown=" + tracked.isShutdown() +
                   " isTerminated=" + tracked.isTerminated());
// Terminated: isShutdown=true  isTerminated=true

// Submitting after shutdown — throws:
try {
    tracked.submit(() -> "too late");
} catch (RejectedExecutionException e) {
    System.out.println("Rejected after shutdown");
}

// ── ThreadPoolExecutor monitoring API ─────────────────────────────────
ThreadPoolExecutor monitor = new ThreadPoolExecutor(
    2, 8, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)
);

// Submit tasks to populate the pool:
for (int i = 0; i < 5; i++) {
    monitor.submit(() -> {
        try { Thread.sleep(500); } catch (InterruptedException e) {}
    });
}
Thread.sleep(100);   // let threads start

System.out.println("Pool size:        " + monitor.getPoolSize());       // threads in pool
System.out.println("Active count:     " + monitor.getActiveCount());    // executing now
System.out.println("Task count:       " + monitor.getTaskCount());      // total submitted
System.out.println("Completed:        " + monitor.getCompletedTaskCount()); // finished
System.out.println("Queue depth:      " + monitor.getQueue().size());   // waiting in queue
System.out.println("Core pool size:   " + monitor.getCorePoolSize());   // min threads
System.out.println("Max pool size:    " + monitor.getMaximumPoolSize()); // max threads

// Periodic monitoring export (production pattern):
ScheduledExecutorService metricsScheduler = Executors.newSingleThreadScheduledExecutor();
metricsScheduler.scheduleAtFixedRate(() -> {
    System.out.printf("[metrics] pool=%d active=%d queued=%d completed=%d%n",
        monitor.getPoolSize(),
        monitor.getActiveCount(),
        monitor.getQueue().size(),
        monitor.getCompletedTaskCount()
    );
}, 0, 30, TimeUnit.SECONDS);

monitor.shutdown();
metricsScheduler.shutdown();

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Thread Basics
A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.
Creating Threads
Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.