Concurrency - ExecutorService
Learn thread pools with ExecutorService, Callable and Future, and graceful shutdown patterns for backend services.
#java #concurrency #executor #futures
Why this step matters
Creating threads manually for every task does not scale.
ExecutorService gives controlled concurrency through thread pools.
Thread pools
Use a pool to reuse threads and limit resource usage.
ExecutorService pool = Executors.newFixedThreadPool(4);
Typical choices:
- fixed pool for stable workloads
- cached pool for bursty short tasks
- scheduled pool for periodic jobs
Runnable vs Callable
Runnable: no resultCallable<T>: returns a result and can throw checked exceptions
Callable<Integer> task = () -> 21 * 2;
Future<Integer> future = pool.submit(task);
Integer value = future.get();
Future basics
Future lets you:
- wait for completion (
get()) - check status (
isDone()) - cancel tasks (
cancel(true))
Avoid blocking everywhere with get(); use timeouts when possible.
Integer value = future.get(1, TimeUnit.SECONDS);
Graceful shutdown
Always close executors cleanly.
pool.shutdown();
if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
pool.shutdownNow();
}
This prevents resource leaks and hanging JVM shutdown.
Common mistakes
- never shutting down the pool
- using unbounded pools in production
- blocking inside pool tasks for long I/O without sizing strategy
- swallowing exceptions from futures
Practical rule
Use ExecutorService as the default low-level concurrency tool when you need explicit thread-pool control.
Takeaway
- Prefer pools over ad-hoc threads
- Use
Callable/Futurefor result-based tasks - Apply timeouts on blocking waits
- Always implement graceful shutdown