Compare LinkedBlockingQueue, ArrayBlockingQueue, SynchronousQueue, and PriorityBlockingQueue as the work queue of a thread pool. How does each affect pool growth and backpressure?
answer
- Linked (no-arg) = unbounded => core-only, OOM risk
- Array = bounded => growth then rejection, real backpressure
- Synchronous = zero capacity => direct hand-off, forces threads
- Priority = unbounded heap, reorders, not FIFO
- Bounded queue is the prerequisite for rejection
basics
~20 sLinkedBlockingQueue is usually unbounded, so the pool stays at core size and can grow memory unbounded. ArrayBlockingQueue is bounded, so it can trigger pool growth and rejection. SynchronousQueue holds nothing and hands tasks directly to a thread, forcing new threads. PriorityBlockingQueue orders tasks by priority and is unbounded.
solid answer
~50 sThe queue determines when the pool grows and whether tasks get rejected. LinkedBlockingQueue defaults to unbounded (Integer.MAX_VALUE): tasks always queue, so the pool never exceeds corePoolSize and maximumPoolSize is ignored — risking memory growth instead of rejection. ArrayBlockingQueue is fixed-capacity: once full it lets the pool grow to max, then rejects, giving bounded memory and real backpressure. SynchronousQueue has zero capacity — every offer must be matched by a waiting taker, so each task either reuses an idle thread or forces a new one up to max, then rejects; this is what Executors.newCachedThreadPool uses. PriorityBlockingQueue is an unbounded heap that reorders tasks by a Comparator/Comparable rather than FIFO, so high-priority tasks run first, but like other unbounded queues it disables max and can grow memory. Choose bounded queues plus a rejection policy when you need predictable resource use.
go deeper
Can name the four queues and say which are bounded vs unbounded.
Can explain how each queue changes pool growth and whether rejection can occur, and map them to the standard Executors factories.
Can pick a queue+policy combination for a given workload, reasoning about memory, throughput (single vs dual lock), and backpressure trade-offs.
Treats queue choice as part of overload and capacity strategy across services, accounting for priority inversion, memory ceilings, and the operational consequences of OOM vs rejection.
## Background A `ThreadPoolExecutor` keeps a `BlockingQueue<Runnable>` between task submitters and worker threads. Recall the growth rule: **core threads first → enqueue → grow to max only when the queue refuses → reject when max is reached and the queue is full**. So the queue's *capacity* and *acceptance behavior* are what actually control pool growth, backpressure, and rejection. Here are the four standard choices. ### 1. LinkedBlockingQueue A linked-node FIFO queue. Its key property: the no-arg constructor makes it **unbounded** (capacity `Integer.MAX_VALUE`). Used as a pool's queue, `offer` essentially never fails, so: - The pool **never grows past `corePoolSize`** (the 'queue full' branch is unreachable). - `maximumPoolSize` becomes dead configuration. - Rejection effectively **cannot happen** — instead, under sustained overload the queue grows, consuming heap until `OutOfMemoryError`. This is the queue behind `Executors.newFixedThreadPool` and `newSingleThreadExecutor`. You can pass an explicit capacity to make it bounded. It uses two separate locks (put/take), so it has higher throughput than ArrayBlockingQueue under contention. ### 2. ArrayBlockingQueue A **bounded**, array-backed FIFO ring buffer; capacity is **mandatory** at construction and fixed forever. With a pool this restores the full algorithm: once the queue's N slots fill, the executor starts growing the pool toward `maximumPoolSize`, and once that ceiling is hit too, tasks are **rejected** via the handler. This gives **bounded memory** and genuine **backpressure** — the system pushes back instead of silently ballooning. It uses a single lock for both ends (lower throughput than LinkedBlockingQueue, but simpler and predictable). ### 3. SynchronousQueue A queue with **zero capacity** — it holds *no* elements. An `offer` succeeds only if another thread is *already waiting* in `take()` to receive it (a direct hand-off / rendezvous). In a pool: - If an idle worker is waiting, the task is handed straight to it. - If not, `offer` fails immediately, so the executor **creates a new thread** (up to `maximumPoolSize`). - Beyond max, tasks are **rejected**. This is what `Executors.newCachedThreadPool` uses — combined with `maximumPoolSize = Integer.MAX_VALUE`, it spawns a thread per task whenever none is free, and reaps idle threads after 60s. Great for many short, bursty tasks; dangerous as unbounded thread creation if work outpaces completion. ### 4. PriorityBlockingQueue An **unbounded** binary-heap queue that orders elements by a `Comparator` (or their `Comparable` natural order) instead of FIFO. Higher-priority tasks are dequeued first. Caveats: it is unbounded (so, like LinkedBlockingQueue, it disables `maximumPoolSize` and can grow memory), it is **not FIFO** so equal-priority ordering is unspecified, and tasks must be comparable (wrap `Runnable` in a comparable type, or use a custom `FutureTask`). Useful when some work must jump the line, accepting the loss of bounded growth. ## Summary table | Queue | Bounded? | Pool grows past core? | Rejection possible? | Ordering | |---|---|---|---|---| | LinkedBlockingQueue (no-arg) | No | No | No (OOM risk) | FIFO | | ArrayBlockingQueue | Yes (fixed) | Yes, after full | Yes | FIFO | | SynchronousQueue | Zero capacity | Yes, immediately | Yes | hand-off | | PriorityBlockingQueue | No | No | No (OOM risk) | priority | ## Practical guidance If you care about predictable memory and want overload to be visible, use a **bounded** ArrayBlockingQueue (or bounded LinkedBlockingQueue) plus an explicit rejection policy. Reserve SynchronousQueue for short-lived bursty tasks with a real max. Treat unbounded queues as a deliberate decision to trade rejection for potential memory growth.
- Why does newCachedThreadPool use a SynchronousQueue with maximumPoolSize = Integer.MAX_VALUE?Because the SynchronousQueue refuses any task that has no idle taker, the executor immediately creates a new thread for each unhandled task; with an effectively infinite max it can scale threads to demand and reaps them after 60s idle. The risk is unbounded thread creation under sustained load.
- How do you make a PriorityBlockingQueue work when submitting plain Runnables?Plain Runnables aren't Comparable, so the heap can't order them. You either submit tasks that implement Comparable, supply a Comparator to the queue, or wrap each task in a comparable wrapper (e.g. a custom Comparable FutureTask) carrying a priority field.
saying these in an interview costs you the question
- Saying LinkedBlockingQueue is bounded by default (its no-arg form is unbounded)
- Thinking SynchronousQueue stores one element (it stores zero — pure hand-off)
- Forgetting that PriorityBlockingQueue is unbounded and disables max
- Claiming ArrayBlockingQueue capacity can be resized after construction