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
// ── 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
// ── 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
// ── 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();