Concurrency API
ExecutorService, Future, CompletableFuture and ForkJoinPool
The Java Concurrency API is the standard toolbox for task-based concurrency. It replaces ad-hoc thread creation with explicit executors, result handles, asynchronous composition, and structured pool lifecycle. Interviewers use this topic to test whether you can reason about overload, backpressure, cancellation, and pool ownership instead of only naming classes.
1. Definition
What is the Concurrency API?
It is the set of standard Java abstractions for running tasks without manually managing raw threads for every unit of work.
Core pieces include:
ExecutorExecutorServiceScheduledExecutorServiceFutureCompletableFutureForkJoinPool
Why this matters
A raw thread couples:
- task submission
- execution resource
- lifecycle
- shutdown semantics
The Concurrency API separates those concerns.
That enables reasoning about:
- capacity
- queueing
- rejection
- cancellation
- result propagation
- ownership
What does a strong answer say?
A strong answer explains:
- why executors are usually preferred to raw threads
- why pool sizing and queueing are architectural decisions
- why
Futureis limited for composition - why
CompletableFutureimproves orchestration - why
ForkJoinPoolis good for CPU-bound decomposition but risky for blocking work
2. Core Concepts
2.1 `Executor` and `ExecutorService`
Executor is the minimal abstraction:
- run this task somehow
ExecutorService adds:
- submission APIs
- shutdown APIs
Futureresults- bulk task methods
The key design question is not only which factory method you use.
It is also:
- how many threads exist?
- what queue type is used?
- what happens under overload?
2.1.1 Keywords and contracts you should state explicitly here
These are the important contract words for this topic:
Executor— minimal task execution abstractionExecutorService— executor with lifecycle and result handlingThreadPoolExecutor— configurable pool implementation- queue capacity — backlog boundary before rejection or pressure appears
- rejection policy — what happens when the pool cannot accept more work
Future— handle for a pending result- cancellation — request to stop or abandon a task result
CompletableFuture— composable asynchronous stage APIForkJoinPool— work-stealing pool for fine-grained tasks- work stealing — idle workers pull tasks from others
- common pool — shared default
ForkJoinPool shutdown()— graceful no-more-submissions shutdownshutdownNow()— forceful interruption-oriented shutdown attempt
2.2 `Future` versus `CompletableFuture`
Future is useful when you need:
- one pending result
- blocking wait with
get() - cancellation
Its weakness is composition.
Chaining many Future values often produces nested blocking or awkward orchestration.
CompletableFuture models dependent stages.
You can:
- transform results
- combine results
- recover from failure
- apply timeouts
- build asynchronous pipelines
2.3 Pool behavior and overload
A fixed-size pool with an unbounded queue controls thread growth.
But it may hide overload as:
- rising latency
- growing queue memory
- delayed failure
A cached or elastic pool reduces queueing.
But under burst it may create too many threads.
That means queue policy is not an implementation detail.
It is part of the service contract.
2.4 `ForkJoinPool`
ForkJoinPool is optimized for recursive decomposition and fine-grained CPU-bound tasks.
Workers maintain deques.
Idle workers steal tasks from busy workers.
This is efficient for divide-and-conquer workloads.
It is usually a poor default for blocking I/O-heavy tasks unless you deeply understand the trade-offs.
3. Practical Usage
Explicit pool ownership
If your component creates a pool, it usually owns its shutdown.
If the pool is shared infrastructure, your component should not casually close it.
That ownership boundary should be obvious in code.
Production-friendly defaults
Prefer explicit ThreadPoolExecutor configuration when operational behavior matters.
That lets you define:
- thread count
- queue type and size
- thread naming
- rejection policy
- keep-alive behavior
Thread names and rejection policy are not optional polish.
They are observability and overload behavior.
`CompletableFuture` discipline
CompletableFuture is powerful.
But it is easy to abuse.
Every async stage should have a sensible execution context.
If you omit the executor, async stages often use the common pool.
That can mix unrelated workloads.
Graceful shutdown
Correct shutdown usually means:
- stop accepting new work
- wait for in-flight work
- escalate only if needed
Blindly calling shutdownNow() can leave work half-finished.
4. Code Examples
Example 1: Explicit thread pool
ThreadFactory factory = runnable -> {
Thread thread = new Thread(runnable);
thread.setName("pricing-worker-" + THREAD_ID.incrementAndGet());
return thread;
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8,
16,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500),
factory,
new ThreadPoolExecutor.CallerRunsPolicy()
);
Key points:
- explicit bounds
- named workers
- explicit queue
- explicit rejection policy
Example 2: `Future` result handling
Future<Price> future = executor.submit(() -> pricingService.calculate(order));
Price price = future.get(200, TimeUnit.MILLISECONDS);
Key points:
Futureis a single-result handleget()is a blocking boundary- timeout is often part of the caller contract
Example 3: `CompletableFuture` composition
CompletableFuture<Customer> customerFuture = CompletableFuture
.supplyAsync(() -> customerClient.fetch(customerId), ioExecutor);
CompletableFuture<Balance> balanceFuture = CompletableFuture
.supplyAsync(() -> accountClient.fetchBalance(customerId), ioExecutor);
CompletableFuture<Summary> summaryFuture = customerFuture.thenCombine(
balanceFuture,
Summary::new
).orTimeout(300, TimeUnit.MILLISECONDS)
.exceptionally(ex -> Summary.fallback(customerId));
Summary summary = summaryFuture.join();
Key points:
- independent calls run concurrently
- combination is explicit
- timeout and fallback live in the pipeline
Example 4: Graceful shutdown
executor.shutdown();
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
Key points:
- graceful first
- escalation second
- lifecycle is explicit
5. Trade-offs
| Choice | Advantage | Cost or risk |
|---|---|---|
ExecutorService |
Better lifecycle and pooling than raw threads | Requires sizing and ownership decisions |
| Unbounded queue | Limits thread growth | Can hide overload as latency and memory growth |
| Bounded queue | Makes pressure visible | Needs rejection handling policy |
Future |
Simple pending result handle | Poor composition model |
CompletableFuture |
Strong orchestration and recovery tools | Easy to overcomplicate execution flow |
ForkJoinPool |
Efficient for fine-grained CPU work | Risky for blocking workloads |
Practical trade-off analysis
The Concurrency API gives control.
But it forces you to make choices explicitly.
Those choices become production behavior.
That is why the best answer is not:
- “I know
ExecutorServiceexists”
It is:
- “I know what queueing, rejection, and shutdown behavior my service is choosing”
6. Common Mistakes
Mistake 1: Using convenience factories without understanding the pool behavior
Factory methods are easy.
Their operational behavior is not always what you want.
Correct approach:
- understand thread count, queueing, and rejection policy
Mistake 2: Forgetting to shut down owned executors
This leaks threads and keeps services alive unexpectedly.
Correct approach:
- make ownership explicit and shut down gracefully
Mistake 3: Blocking the common pool carelessly
Shared pools can starve unrelated work.
Correct approach:
- use dedicated executors for blocking or business-critical workloads
Mistake 4: Treating `Future` as if it composes well
It does not.
Correct approach:
- use
CompletableFuturewhen orchestration matters
Mistake 5: Spraying async stages everywhere
Asynchrony without clear boundaries complicates tracing and failure handling.
Correct approach:
- use async composition when it expresses real concurrency or latency benefit
Mistake 6: Ignoring overload behavior
A pool under stress needs a policy.
Correct approach:
- decide whether you queue, reject, or push back on callers
7. Deep Dive
7.1 Queueing is architecture
An unbounded queue feels safe because tasks are accepted.
But hidden overload is often worse than visible rejection.
It produces:
- rising latency
- stale work
- memory pressure
- poor tail behavior
7.2 Ownership and shutdown
An executor is an infrastructure resource.
That means its lifecycle must answer:
- who creates it?
- who shuts it down?
- what happens to queued work?
- what happens during application shutdown?
If the answers are unclear, incidents follow.
7.3 `CompletableFuture` as orchestration tool
CompletableFuture is strongest when used to express dependency graphs.
It is weaker when used only as “async everywhere”.
The mature answer emphasizes:
- stage composition
- timeout placement
- fallback boundaries
- executor choice
7.4 `ForkJoinPool` realism
Work stealing is excellent for CPU-bound recursive workloads.
It is not magical.
If tasks block heavily, pool throughput and fairness can suffer.
That is why “use common pool for everything” is a weak answer.
8. Interview Questions
1. Why use `ExecutorService` instead of raw threads?
Because it separates task submission from execution resource management and adds lifecycle control.
2. What does `Future` represent?
A handle to a pending result.
3. Why is `CompletableFuture` often preferred for orchestration?
Because it composes dependent asynchronous stages much more cleanly.
4. Why is queue choice important?
Because queueing changes overload behavior, latency, and memory use.
5. What does a rejection policy do?
It defines what happens when the executor cannot accept more work.
6. Why is thread naming important in pools?
Because it improves diagnostics and observability.
7. When is `ForkJoinPool` a good fit?
For fine-grained CPU-bound recursive work.
8. Why can blocking the common pool be dangerous?
Because unrelated tasks may starve on the shared execution resource.
9. What is a good shutdown sequence for an owned executor?
shutdown(), wait, then escalate if needed.
10. What is the senior-level takeaway?
Pool behavior is part of your system contract, not just an implementation detail.
9. Glossary
| Term | Meaning |
|---|---|
Executor |
Minimal task execution abstraction |
ExecutorService |
Executor with lifecycle and result handling |
ThreadPoolExecutor |
Configurable executor implementation |
Future |
Pending result handle |
CompletableFuture |
Composable asynchronous stage API |
| rejection policy | Behavior when no more work can be accepted |
| bounded queue | Queue with explicit capacity limit |
| common pool | Shared default ForkJoinPool |
| work stealing | Idle workers steal tasks from busy workers |
| graceful shutdown | Stop accepting work while letting in-flight tasks finish |
10. Cheatsheet
- Prefer executors to raw threads for general task dispatch
- Pool sizing and queueing are architectural decisions
- Name worker threads explicitly
- Make rejection behavior explicit
Futureis good for one pending resultCompletableFutureis good for orchestration and recovery- Do not block the common pool carelessly
- Shut down owned executors explicitly
- Use bounded resources when overload behavior matters
- Interjúban nevezd meg a queueing, rejection, ownership és shutdown trade-offokat
🎮 Games
10 questions