Walk through exactly how ThreadPoolExecutor decides whether to create a thread, enqueue a task, or reject it when execute() is called.
answer
- core → queue → max → reject (in that order)
- below core: new thread even if others idle
- grow past core ONLY when queue is full
- queuing is preferred over new threads
- unbounded queue ⇒ max never reached
basics
~20 sIf fewer than core threads exist, it starts a new thread. Otherwise it tries to put the task in the queue. If the queue is full, it starts more threads up to the maximum. If the queue is full and threads are at the maximum, the task is rejected.
solid answer
~50 sOn each execute(), ThreadPoolExecutor applies three steps in order. First, if the current worker count is below corePoolSize, it creates a new core thread to run the task immediately — even if other threads are idle. Second, if at or above core, it tries to offer the task to the workQueue; if the queue accepts it, the task waits there. Third, only if the queue is full does it try to add a non-core thread, up to maximumPoolSize. If the queue is full and the pool is already at maximumPoolSize (or the pool is shut down), the RejectedExecutionHandler runs. The key, often-surprising consequence is that the pool prefers queuing over growing: extra threads beyond core are added only when the queue can't hold the task. This is why an unbounded queue prevents the pool from ever exceeding core.
go deeper
Can state the three outcomes (new thread / queue / reject) even if shaky on the exact order.
States the correct order — fill core, then queue, then grow to max, then reject — and why submitting when the queue has room doesn't spawn a thread.
Derives the unbounded-queue and SynchronousQueue corollaries and explains the OOM risk from this ordering.
Uses the ordering to design backpressure: chooses queue type/size and rejection policy so the system degrades predictably under load rather than queuing to death.
## The question this answers When you call `executor.execute(task)` (or `submit`, which calls `execute` internally), the pool must decide one of three outcomes: **run it on a new thread**, **park it in the queue**, or **reject it**. The order of these checks is fixed and is the single most important — and most counter-intuitive — fact about `ThreadPoolExecutor`. Many production incidents come from not knowing it. ## Key terms - **Worker / pool size** — the current number of live worker threads. - **corePoolSize / maximumPoolSize** — see the parameters question: the baseline kept-alive count and the hard ceiling. - **workQueue** — a `BlockingQueue<Runnable>` of tasks waiting for a free thread. `offer(task)` returns `false` if the queue is *full* (a bounded queue at capacity) or always-false for a *synchronous* queue with no waiting consumer. - **RejectedExecutionHandler** — the policy run when the task can't be accepted. ## The three-step decision (exact order) For every submitted task: **Step 1 — below core size → add a core thread.** If `workerCount < corePoolSize`, the executor starts a **new** worker thread whose first task is this one. This happens *even if some existing threads are idle* — until the core is full, submission grows the pool rather than reusing/queuing. (There is a subtle exception during the first tasks, but conceptually: fill the core first.) **Step 2 — at/above core → try to enqueue.** If the pool already has at least `corePoolSize` threads and is running, it calls `workQueue.offer(task)`. If that **succeeds**, the task sits in the queue and will be picked up by the next free worker. The pool does **not** grow here. (It re-checks state after enqueuing to handle a race where the pool was shutting down or all workers died.) **Step 3 — queue full → add a non-core thread up to max.** If `offer` returns **false** (queue is full or synchronous-with-no-taker), the executor tries to start a new worker via `addWorker(task, false)` — the `false` meaning 'this is beyond the core, bounded by `maximumPoolSize`'. If `workerCount < maximumPoolSize`, a new thread is created to run the task immediately. **Step 4 — can't add → reject.** If the queue is full **and** the pool is already at `maximumPoolSize` (or the executor has been shut down), `addWorker` fails and the task is handed to `reject(task)`, which invokes the `RejectedExecutionHandler`. ## The crucial mental model The pool **prefers queuing to growing**. Threads beyond the core are a *last resort*, used only when the queue refuses the task. Consequences: - **Unbounded queue (e.g. `LinkedBlockingQueue` with no capacity)** → `offer` *never* returns false → Step 3 never triggers → the pool **never grows past `corePoolSize`**, and `maximumPoolSize` is effectively ignored. Tasks pile up in memory under sustained overload — a classic OutOfMemoryError trap. - **Bounded queue** → the pool grows from core toward max only after the buffer fills, giving a natural 'core → buffer → burst threads → reject' staircase. - **Synchronous handoff (`SynchronousQueue`)** → every `offer` fails unless a thread is already waiting, so almost every task tries to add a thread; with a large/unbounded max this behaves like 'one thread per task' (what `newCachedThreadPool` does). ## Why interviewers love this The naive expectation is 'grow to max, *then* queue.' The real behavior is 'fill core, *then* queue, *then* grow to max, *then* reject.' Stating that order correctly — and the unbounded-queue corollary — demonstrates real understanding rather than memorized parameter names.
- Why does an unbounded LinkedBlockingQueue make maximumPoolSize irrelevant?Because the pool only adds threads beyond core when queue.offer() fails. An unbounded queue never rejects an offer, so Step 3 never runs and the pool stays at corePoolSize regardless of the max.
- If 4 core threads are busy and the queue has room, does submitting a 5th task spawn a 5th thread?No. With the queue not full, the task is enqueued, not given a new thread. New threads beyond core appear only once the queue is full.
A restaurant: first seat diners at empty core tables (add core threads). Once core tables are taken, new arrivals wait in the lobby queue. Only when the lobby is packed do you open the overflow room up to a fire-code max (max threads). When the overflow is full and the lobby is full, you turn people away (reject). If the lobby were infinitely large, you'd never open the overflow room.
saying these in an interview costs you the question
- Claiming the pool grows to maximumPoolSize before it starts queuing (the order is reversed).
- Saying tasks are queued first and threads are a fallback at all sizes (below core, a thread is created immediately).
- Not recognizing that an unbounded queue pins the pool at core size.
- Thinking idle threads are reused before creating a core thread when below corePoolSize.