Java ExecutorService: Thread Pools, Callable, and Future
Creating raw threads with new Thread() is low-level and error-prone. ExecutorService manages a pool of worker threads, decoupling task submission from execution. Callable is the return-value-bearing sibling of Runnable; Future holds the eventual result. ScheduledExecutorService adds delay and periodic execution. For non-blocking async chains, combine ExecutorService with CompletableFuture.
Contents
Creating Thread Pools
The ExecutorsExecutorServicenewFixedThreadPool(n)nnewCachedThreadPool()newSingleThreadExecutor()newVirtualThreadPerTaskExecutor()
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 and Future
Callable<V>Runnablecall()VRunnable.run()CallableExecutorServiceFuture<V>future.get()get()ExecutionExceptiongetCause()get(long, TimeUnit)
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 and invokeAny
invokeAll()CallableList<Future>get()isCancelled() == trueinvokeAny()invokeAny
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();
invokeAnyScheduledExecutorService
ScheduledExecutorServiceExecutorServiceschedule()ScheduledFuturescheduleAtFixedRate()scheduleWithFixedDelay()ScheduledFuture
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();
Shutdown and Lifecycle
An ExecutorServiceshutdown()shutdownNow()Thread.currentThread().isInterrupted()awaitTermination(timeout, unit)shutdown()shutdownNow()
// 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();
// }
shutdown()shutdownNow()ExecutorService