skip to content

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