Java Concurrency
What are ExecutorService and thread pools in Java?
ExecutorService separates task submission from thread management. A thread pool reuses a limited number of worker threads instead of creating a new thread for every task.
The Short Answer
ExecutorServiceis Java's higher-level API for submitting work to be executed asynchronously.
A thread pool is a group of reusable worker threads. Instead of creating a brand-new thread for every task, the pool keeps a limited number of threads around and gives them tasks from a queue.
The Real Problem It Solves
Imagine a backend service receiving many requests. For each request, you may need to send an email, call another service, process a file, or run some background work.
You could create a new thread every time:
new Thread(() -> {
sendEmail(user);
}).start();But this does not scale well. Threads are expensive. If traffic spikes, you may create too many threads, waste memory, increase context switching, and overload the server.
Mental Model
The caller does not directly control which thread runs the task. The caller submits work, and the executor schedules it onto available worker threads.
Simple Example
ExecutorService executor =
Executors.newFixedThreadPool(4);
executor.submit(() -> {
System.out.println("Running task on " +
Thread.currentThread().getName());
});
executor.shutdown();This creates a pool with 4 worker threads. Tasks submitted to the executor are run by those workers.
Calling shutdown() is important. It tells the executor to stop accepting new tasks and eventually finish after already-submitted work completes.
Why Thread Pools Matter
Without a Thread Pool
With a Thread Pool
Runnable vs Callable
You usually submit either a Runnable or a Callable.
Runnable task = () -> {
System.out.println("No return value");
};
Callable<Integer> taskWithResult = () -> {
return 42;
};Runnable does not return a value. Callable returns a value and can throw checked exceptions.
Future: Getting the Result Later
ExecutorService executor =
Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(() -> {
return expensiveCalculation();
});
Integer result = future.get();A Future represents a result that may not be ready yet.
Important Production Warning: Queue Growth
A thread pool protects you from creating unlimited threads, but it can still fail if the task queue grows without control.
If tasks arrive faster than workers can process them, the queue gets larger and larger.
A pool with too few workers or an unbounded queue can turn a traffic spike into memory pressure and high latency.
Fixed Thread Pool vs Cached Thread Pool
Fixed Thread Pool
Uses a fixed number of worker threads. Extra tasks wait in the queue.
Executors.newFixedThreadPool(8);Good when you want predictable concurrency limits.
Cached Thread Pool
Creates threads as needed and reuses idle ones when possible.
Executors.newCachedThreadPool();Useful for many short-lived async tasks, but dangerous if task volume explodes.
The Interview-Friendly Explanation
Common Interview Follow-Ups
Why use ExecutorService instead of new Thread?
Because ExecutorService reuses threads, limits concurrency, manages task submission, and gives APIs like submit(), Future, shutdown(), and awaitTermination().
What is the difference between execute() and submit()?
execute() runs a Runnable and does not return a result. submit() returns a Future, which can represent completion, result, or failure.
Why is forgetting shutdown() a problem?
Executor threads may keep running and prevent the application from exiting cleanly. In services, it can also leave resources alive longer than intended.
Is a bigger thread pool always better?
No. Too many threads can increase context switching, memory usage, contention, and latency. Pool size depends on CPU cores, blocking behavior, workload type, and downstream limits.
What happens when all threads are busy?
New tasks usually wait in a queue. Depending on the executor configuration, the queue can grow, reject tasks, or apply a custom rejection policy.