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.

ConcurrencyExecutorServiceThread PoolsBackend

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 key idea: you submit tasks, and the executor manages the threads.

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:

java
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.

ExecutorService gives you control. Instead of unlimited thread creation, you submit work to a managed pool.

Mental Model

Incoming Tasks
go into
Task Queue
picked up by reusable
Worker 1
Worker 2
Worker 3
Worker 4

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

java
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

Request 1 → new Thread
Request 2 → new Thread
Request 3 → new Thread
Traffic spike → too many threads

With a Thread Pool

Requests become tasks
Tasks wait in queue
Fixed workers process them

Runnable vs Callable

You usually submit either a Runnable or a Callable.

java
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

java
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.

Be careful: future.get() blocks the current thread until the result is available.

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.

Task
Task
Task
Task
More...

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.

java
Executors.newFixedThreadPool(8);

Good when you want predictable concurrency limits.

Cached Thread Pool

Creates threads as needed and reuses idle ones when possible.

java
Executors.newCachedThreadPool();

Useful for many short-lived async tasks, but dangerous if task volume explodes.

The Interview-Friendly Explanation

ExecutorService decouples task submission from thread management. Instead of manually creating threads, you submit Runnable or Callable tasks. A thread pool reuses worker threads, controls concurrency, and queues extra work. This improves performance and prevents uncontrolled thread creation, but you still need to size the pool, manage queue growth, handle failures, and shut it down correctly.

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.

Final Takeaway

ExecutorService is not just a convenience API. It is a way to control concurrency in production systems: how much work runs now, how much waits, and how gracefully the system behaves under load.