Contents

The Executors class provides factory methods for the most common thread pool configurations, all returning an ExecutorService. newFixedThreadPool(n) creates a pool with exactly n threads — tasks beyond that queue up, giving you bounded concurrency that scales predictably with CPU cores. newCachedThreadPool() creates threads on demand and reuses idle ones, making it efficient for short-lived bursty tasks but risky if tasks are long-running because thread count is unbounded. newSingleThreadExecutor() uses one thread and executes tasks sequentially in submission order, effectively serializing access to a shared resource without explicit locking. For Java 21 and later, newVirtualThreadPerTaskExecutor() assigns a lightweight virtual thread to every submitted task, removing the need to size thread pools for I/O-bound workloads.

import java.util.concurrent.*; // Executors factory methods — the right pool type for the job // Fixed thread pool — capped at N threads; excess tasks queue up ExecutorService fixed = Executors.newFixedThreadPool(4); // Use: CPU-bound tasks where you want to limit parallelism to core count // Cached thread pool — creates threads on demand; idle threads die after 60s ExecutorService cached = Executors.newCachedThreadPool(); // Use: short-lived tasks with bursty load; risky for long tasks (unbounded threads) // Single-thread executor — one thread, tasks run sequentially ExecutorService single = Executors.newSingleThreadExecutor(); // Use: serialized access to a shared resource (e.g., log writer) // Work-stealing pool — ForkJoinPool; parallelism = CPU cores ExecutorService workStealing = Executors.newWorkStealingPool(); // Use: recursive decomposable tasks (same as ForkJoinPool.commonPool()) // Virtual thread executor — Java 21 (Project Loom) ExecutorService vthreads = Executors.newVirtualThreadPerTaskExecutor(); // Use: high-concurrency I/O-bound tasks; each task gets its own virtual thread // Submit a simple Runnable (no return value) fixed.submit(() -> System.out.println("Task on thread: " + Thread.currentThread().getName())); fixed.execute(() -> System.out.println("Another task")); // execute() vs submit() — no Future // Always shut down the executor when done fixed.shutdown();

Callable<V> is the result-bearing counterpart to Runnable: its call() method returns a value of type V and is permitted to throw checked exceptions, unlike Runnable.run(). Submitting a Callable to an ExecutorService returns a Future<V> immediately — the task runs asynchronously while the caller continues. Calling future.get() blocks until the result is available; if the task threw an exception, get() wraps it in an ExecutionException, so always inspect getCause() to find the actual error. Always use the timeout overload get(long, TimeUnit) in production code to avoid blocking forever if a task hangs; cancel the future if the timeout expires.

ExecutorService pool = Executors.newFixedThreadPool(4); // Callable — like Runnable but returns a result and can throw checked exceptions Callable<Integer> task = () -> { Thread.sleep(1000); // simulate work return 42; }; // submit(Callable) returns a Future Future<Integer> future = pool.submit(task); System.out.println("Task submitted, doing other work..."); // future.get() blocks until the result is available try { Integer result = future.get(); // blocks here System.out.println("Result: " + result); // 42 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { System.err.println("Task threw: " + e.getCause()); // actual exception } // get with timeout — don't wait forever try { Integer result = future.get(2, TimeUnit.SECONDS); } catch (TimeoutException e) { future.cancel(true); // interrupt the task System.err.println("Task timed out"); } // isDone, isCancelled System.out.println(future.isDone()); // true once result available System.out.println(future.isCancelled()); // true if cancelled // Cancel a task (may interrupt if running) future.cancel(true); // true = interrupt if running // Submit multiple tasks List<Future<String>> futures = new ArrayList<>(); for (int i = 0; i < 5; i++) { final int id = i; futures.add(pool.submit(() -> "Result-" + id)); } for (Future<String> f : futures) { System.out.println(f.get()); // collect results in submission order } pool.shutdown();

invokeAll() is a convenience method that submits a collection of Callable tasks at once and blocks until every task has completed (or the optional timeout elapses). It returns a List<Future> in the same order as the input collection; each future is already done when the list is returned, so get() returns immediately. Tasks that exceeded the timeout are cancelled and their futures will report isCancelled() == true. invokeAny() takes the same collection but races them: it blocks only until the first task completes successfully, returns that result directly, and cancels all remaining tasks. Use invokeAny for hedged requests — submitting the same query to multiple replicas or endpoints and taking whichever answers first.

ExecutorService pool = Executors.newFixedThreadPool(4); List<Callable<String>> tasks = List.of( () -> { Thread.sleep(300); return "task-1"; }, () -> { Thread.sleep(100); return "task-2"; }, () -> { Thread.sleep(200); return "task-3"; } ); // invokeAll — submit all tasks; blocks until ALL complete (or timeout) // Returns a List<Future> in the same order as input tasks List<Future<String>> results = pool.invokeAll(tasks); for (Future<String> f : results) { System.out.println(f.get()); // task-1, task-2, task-3 in order } // invokeAll with timeout — tasks not done by deadline are cancelled List<Future<String>> timedResults = pool.invokeAll(tasks, 500, TimeUnit.MILLISECONDS); for (Future<String> f : timedResults) { if (f.isDone() && !f.isCancelled()) { System.out.println(f.get()); } else { System.out.println("cancelled/timed out"); } } // invokeAny — submit all tasks; returns result of the FIRST successful one // Other tasks are cancelled. Use when any result will do (e.g., racing servers) String first = pool.invokeAny(tasks); System.out.println("Fastest result: " + first); // "task-2" (100ms wait) // This is the "racing futures" pattern pool.shutdown(); invokeAny is perfect for implementing fallback strategies — submit the same request to multiple service endpoints and use whichever responds first. The slower requests are automatically cancelled.

ScheduledExecutorService extends ExecutorService with three scheduling methods. schedule() runs a task once after a specified delay and returns a ScheduledFuture you can cancel or inspect. scheduleAtFixedRate() fires at a fixed calendar interval regardless of how long each execution takes — if a task runs longer than its period, the next execution starts immediately after it finishes rather than stacking up. scheduleWithFixedDelay() inserts the fixed delay between the end of one execution and the start of the next, so the inter-execution gap is always at least the delay duration; this is safer when task duration is variable and you want to avoid overlapping runs. Both repeating methods continue indefinitely until the returned ScheduledFuture is cancelled or the scheduler is shut down.

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); // schedule — run once after a delay ScheduledFuture<?> delayed = scheduler.schedule( () -> System.out.println("Delayed task"), 5, TimeUnit.SECONDS ); // scheduleAtFixedRate — run at fixed intervals from initial delay // Period = time between start of consecutive executions // If task takes longer than period, next run starts immediately after ScheduledFuture<?> heartbeat = scheduler.scheduleAtFixedRate( () -> System.out.println("Heartbeat: " + System.currentTimeMillis()), 0, // initial delay 1, // period TimeUnit.SECONDS ); // scheduleWithFixedDelay — run with fixed delay BETWEEN executions // Delay = time between end of one execution and start of the next ScheduledFuture<?> poller = scheduler.scheduleWithFixedDelay( () -> pollDatabase(), // if this takes 2s, next run starts 3s after it finishes 0, // initial delay 3, // delay between end of one run and start of next TimeUnit.SECONDS ); // Cancel individual tasks heartbeat.cancel(false); // false = let current execution finish // Cancel at specific time (compute delay from now) LocalDateTime nextRun = LocalDateTime.now().plusDays(1).withHour(9).withMinute(0); long secondsUntil = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextRun); scheduler.schedule(() -> generateReport(), secondsUntil, TimeUnit.SECONDS); // Shut down cleanly scheduler.shutdown();

An ExecutorService owns threads that keep the JVM alive until they exit. shutdown() initiates an orderly shutdown: no new tasks are accepted, but tasks already submitted continue to run. shutdownNow() goes further by sending an interrupt to every running thread and returning the list of queued tasks that were never started; tasks must cooperate with interruption by checking Thread.currentThread().isInterrupted() or blocking on interruptible operations for this to take effect promptly. awaitTermination(timeout, unit) blocks the calling thread until all tasks finish or the timeout expires — combine the two in a try/finally block to handle the common escalation pattern: ask nicely with shutdown(), wait a reasonable time, then force with shutdownNow(). Skipping shutdown is the most common cause of thread leaks in application servers and hanging test suites.

// Graceful shutdown pattern — the correct way to stop an executor ExecutorService pool = Executors.newFixedThreadPool(4); // ... submit tasks ... // Step 1: shutdown() — no new tasks accepted; queued tasks still run pool.shutdown(); try { // Step 2: wait for in-progress tasks to complete (up to timeout) if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { // Step 3: if still running after timeout, force stop pool.shutdownNow(); // sends interrupt to running tasks // Step 4: wait a bit more for interrupt-responsive tasks to stop if (!pool.awaitTermination(10, TimeUnit.SECONDS)) { System.err.println("Pool did not terminate"); } } } catch (InterruptedException ex) { pool.shutdownNow(); Thread.currentThread().interrupt(); } // shutdownNow() — attempts to stop all running tasks by interrupting threads // Returns a list of queued tasks that were never started List<Runnable> notStarted = pool.shutdownNow(); // isShutdown() — true after shutdown() called (but tasks may still be running) // isTerminated() — true after all tasks have completed post-shutdown // Thread factory — customize thread properties (name, priority, daemon) ThreadFactory namedFactory = r -> { Thread t = new Thread(r, "worker-pool"); t.setDaemon(true); // daemon threads don't prevent JVM exit t.setPriority(Thread.NORM_PRIORITY); return t; }; ExecutorService named = Executors.newFixedThreadPool(4, namedFactory); // Java 19+: structured concurrency (preview) — better lifecycle management // try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // Future<String> f1 = scope.fork(() -> task1()); // Future<String> f2 = scope.fork(() -> task2()); // scope.join(); scope.throwIfFailed(); // } Always call shutdown() or shutdownNow() when done with an ExecutorService. Non-daemon thread pool threads keep the JVM alive indefinitely if not shut down — a common source of hanging processes in application servers and test suites.