skip to content

Executor & ExecutorService

Executor decouples task submission from execution, and ExecutorService adds lifecycle plus result-bearing submit. Interviewers ask about submit versus execute specifically because submit captures the exception in the Future, where it is easy to lose.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What is the Executor interface, and what problem does it solve compared to creating threads directly?

level: juniorimportance: must knowfreq 70%

answer

  1. Single method: execute(Runnable)
  2. Decouples WHAT (task) from HOW (execution policy)
  3. Avoids per-task new Thread().start()
  4. Reuse + bounded + backpressure
  5. Root of ExecutorService / Executors factory

basics

~20 s

Executor is a tiny interface with one method, execute(Runnable). You hand it a task to run; it decides how and when to run it. This separates submitting work from creating threads, so you don't call new Thread(...).start() everywhere.

solid answer

~40 s

Executor is the base abstraction of the java.util.concurrent task framework. It has a single method, execute(Runnable command), which runs the given task at some point. The key idea is decoupling: callers submit tasks (the 'what') without knowing how they're executed (the 'how') — same thread, a new thread, or a pooled thread. This lets you swap execution policy (thread pool size, queuing, scheduling) without touching task-submission code. Directly using new Thread(r).start() couples your code to one-thread-per-task, which is wasteful (thread creation is expensive) and unbounded (no backpressure, you can exhaust memory). Executor centralizes that policy. In practice you rarely use bare Executor; you use ExecutorService (its richer subinterface) obtained from the Executors factory, but Executor is the conceptual root.

go deeper

for a junior

Knows Executor has one method, execute(Runnable), and that it lets you run tasks without writing new Thread().start() yourself.

for a middle

Explains the decoupling of submission from execution policy, why per-task threads are wasteful/unbounded, and how a pool gives reuse and backpressure.

for a senior

Articulates the contract precisely (no guarantee of which thread or when), relates Executor to ExecutorService/Executors, and discusses execution-policy trade-offs.

for a principal

Frames Executor as a policy seam enabling system-wide resource governance and backpressure design; reasons about when a direct/caller-runs executor is the right policy and how the abstraction shapes API design.

## The problem: threads are expensive and uncontrolled A **thread** is the unit of independent execution managed by the OS/JVM. The naive way to run a task concurrently in Java is: ```java new Thread(() -> doWork()).start(); ``` This works but has serious downsides: - **Cost:** creating and destroying a thread is expensive (memory for its stack — typically ~512KB–1MB — plus OS scheduling setup). Doing it per task wastes resources. - **Unbounded:** if 10,000 requests arrive, you create 10,000 threads. There is no limit, so you can exhaust memory and crash. There is no **backpressure** (a way to slow down submitters when the system is overloaded). - **Coupling:** the code that *submits* work is tangled with the code that decides *how* it runs. You can't change the strategy (reuse threads? cap the count? schedule later?) without editing every call site. ## The Executor interface Java's answer (added in Java 5, package `java.util.concurrent`) is the **Executor** interface — deliberately minimal: ```java public interface Executor { void execute(Runnable command); } ``` A **Runnable** is just an object with a `run()` method — a task that takes no arguments and returns nothing. `execute` accepts such a task and runs it *at some point in the future*, according to the executor's own **execution policy**. The interface makes **no promise** about *which* thread runs it or *when*. A valid Executor could: - run the task immediately in the calling thread (synchronous); - spawn a brand-new thread per task; - hand it to a reusable **thread pool** (the common case). The whole point is **decoupling task submission from task execution**. The caller says *what* to do; the Executor decides *how*. This is an instance of the producer–consumer pattern: submitters produce tasks, worker threads consume them. ## Why this matters Because execution policy lives behind the interface, you can change it in one place. Want to cap concurrency at 8 threads with a bounded queue that rejects overflow? Configure that on the Executor; the thousands of `execute(task)` call sites don't change. This gives you **resource management**, **reuse** (amortize thread-creation cost across many tasks), and **backpressure** (a bounded queue + rejection policy slows or signals overloaded submitters). ## Where it sits in the hierarchy `Executor` is the root. Its subinterface **ExecutorService** adds lifecycle (shutdown) and result-bearing submission (`submit` returning a `Future`). You almost always program against `ExecutorService`, obtained from the **Executors** factory class (e.g. `Executors.newFixedThreadPool(n)`). But conceptually, `Executor` is the one-method seam that everything else builds on. ## Tiny example ```java Executor executor = Executors.newFixedThreadPool(4); // 4 reusable worker threads executor.execute(() -> System.out.println("ran on " + Thread.currentThread().getName())); ``` The lambda is a `Runnable`; `execute` queues it for one of the four pooled threads. No `new Thread` in sight, and the pool reuses its threads across many tasks.

  • Why does execute() take a Runnable and not a Callable?
    Executor is the minimal base abstraction for fire-and-forget tasks, so it uses Runnable (no result, no checked exception). Result-bearing submission via Callable is added one level up in ExecutorService.submit.
  • Could execute() run the task synchronously in the calling thread?
    Yes. The contract doesn't require asynchrony. A direct executor that just calls command.run() is legal and is sometimes used in tests or as a ThreadPoolExecutor rejection policy (CallerRunsPolicy).

saying these in an interview costs you the question

  • Saying Executor guarantees a new/separate thread — it makes no such promise; a valid Executor can run in the caller's thread.
  • Thinking execute() returns a result or a Future — it returns void; Future comes from ExecutorService.submit.
  • Claiming new Thread per task is fine at scale — it's unbounded and wasteful.
  • Confusing Executor (interface) with Executors (factory class).

context

open as a page

What is the difference between Executor.execute(Runnable) and ExecutorService.submit(...)? Pay special attention to return values and exception handling.

level: middleimportance: must knowfreq 80%

basics

~20 s

execute runs a Runnable and returns nothing (fire-and-forget); if it throws, the exception goes to the thread's uncaught-exception handler. submit accepts a Runnable or Callable and returns a Future you can wait on; any exception is captured inside that Future and re-thrown only when you call get().

open as a page

ExecutorService extends Executor with a lifecycle. Walk through shutting one down correctly, contrasting shutdown(), shutdownNow(), and awaitTermination().

level: middleimportance: must knowfreq 68%

basics

~20 s

ExecutorService adds a lifecycle on top of Executor. shutdown() stops accepting new tasks but lets queued ones finish. shutdownNow() tries to stop immediately and returns the unstarted tasks. awaitTermination() blocks until everything finishes or a timeout passes. You must shut it down, or its threads keep the JVM alive.

open as a page

Explain invokeAll and invokeAny on ExecutorService: their blocking and return semantics, how they handle exceptions and cancellation, and when you'd reach for each.

level: seniorimportance: should knowfreq 52%

basics

~20 s

Both take a collection of Callables. invokeAll runs them all and blocks until every one finishes, returning a list of Futures in the same order. invokeAny runs them and blocks until the first one succeeds, returning that single result and cancelling the rest. Use invokeAll for fan-out/gather; invokeAny when any one good answer suffices.

open as a page

As a system grows, why is choosing and owning the Executor's execution policy (pool sizing, queue, rejection) more important than which submission method you call? How does the Executor abstraction help, and where does it fall short?

level: principalimportance: should knowfreq 38%

basics

~20 s

execute vs submit is a small local choice. What really controls behavior under load is the execution policy: how many threads, how big the queue, and what happens when both are full. The Executor interface hides that policy behind one method, so you can set it centrally — but it can't pick the right numbers for you or stop teams from sharing one pool unsafely.

open as a page