skip to content

Compare ArrayBlockingQueue and LinkedBlockingQueue. When would you choose each?

level: middleimportance: must knowfreq 72%

answer

  1. Array = fixed array, single lock, always bounded
  2. Linked = nodes, two locks (put/take), optionally bounded
  3. Two locks → parallel put+take → throughput
  4. Default LinkedBlockingQueue is UNBOUNDED → OOM risk
  5. Array has optional fairness + less GC

basics

~20 s

ArrayBlockingQueue is backed by a fixed-size array and is always bounded. LinkedBlockingQueue uses linked nodes and is optionally bounded (unbounded by default). ArrayBlockingQueue uses one lock; LinkedBlockingQueue uses two (put and take), so it often has higher throughput under contention.

solid answer

~50 s

Both are FIFO. ArrayBlockingQueue wraps a fixed circular array, so its capacity is set at construction and never changes — it's always bounded and allocates its storage up front. LinkedBlockingQueue is a linked-node queue, optionally bounded (capacity defaults to Integer.MAX_VALUE, effectively unbounded), allocating nodes on demand. The key concurrency difference: ArrayBlockingQueue guards the whole queue with a single ReentrantLock, so puts and takes contend with each other; LinkedBlockingQueue uses two separate locks (a putLock and a takeLock), letting a producer and consumer proceed in parallel, which raises throughput under contention. Trade-offs: prefer ArrayBlockingQueue when you want a tight, predictable memory bound and lower per-element overhead (no node objects, no GC churn); prefer LinkedBlockingQueue for higher concurrent throughput or when you genuinely want unbounded — but an unbounded one risks OutOfMemoryError if producers outrun consumers. ArrayBlockingQueue also offers an optional fairness flag for FIFO lock acquisition.

code

java · 8 lines
java
// Bounded array queue: capacity fixed at construction, single lock, optional fairness
BlockingQueue<Task> aq = new ArrayBlockingQueue<>(1000, /*fair=*/ true);

// Linked queue, EXPLICITLY bounded for backpressure (two-lock, higher throughput)
BlockingQueue<Task> lqBounded = new LinkedBlockingQueue<>(1000);

// DANGER: unbounded by default — producers outpacing consumers can OOM
BlockingQueue<Task> lqUnbounded = new LinkedBlockingQueue<>();

go deeper

for a junior

Knows both are FIFO; one is array-backed and bounded, the other linked and can be unbounded.

for a middle

Explains the single-lock vs two-lock difference, the unbounded default of LinkedBlockingQueue, and basic memory/throughput trade-offs.

for a senior

Chooses deliberately for backpressure and footprint, knows the GC implications and the newFixedThreadPool OOM pitfall, uses fairness when starvation matters.

for a principal

Reasons about lock contention profiles under real workloads, sizes capacity as a system-stability parameter, and weighs these against lock-free/reactive alternatives for the throughput target.

## Both are FIFO BlockingQueues Both implement `BlockingQueue<E>` and order elements **first-in-first-out** (the head is the element that's been waiting longest). They differ in internal structure and locking, which drives the choice. ## ArrayBlockingQueue - **Backed by a fixed-size array** used as a *circular buffer* (two indices, `putIndex` and `takeIndex`, wrap around). - **Always bounded**: you must pass a capacity to the constructor and it cannot change. Storage is allocated **up front** as one array. - **Single lock**: one `ReentrantLock` protects the whole queue, with two `Condition`s (`notEmpty`, `notFull`) for waiting consumers/producers. Because there's one lock, a `put` and a `take` **cannot run simultaneously** — they serialize. - **Optional fairness**: the constructor takes a `fair` boolean; when true, threads waiting on the lock are served in FIFO order (avoids starvation at some throughput cost). - **Lower per-element overhead**: no wrapper node objects, so less memory and **less GC pressure**; memory footprint is fixed and predictable. ## LinkedBlockingQueue - **Backed by linked nodes** (a singly linked list); each element is wrapped in a `Node` object allocated on demand. - **Optionally bounded**: the no-arg constructor sets capacity to `Integer.MAX_VALUE` — *effectively unbounded*. You can pass a capacity to bound it. - **Two locks** (the 'two-lock queue' design): a `putLock` guards the tail, a `takeLock` guards the head. A producer and a consumer can therefore operate **in parallel** on opposite ends, raising throughput when both sides are busy. A `count` is kept as an `AtomicInteger` because the two locks each touch it. - **More overhead**: node allocation per element means more objects and **GC churn**; the head/tail are not pre-allocated. ## How to choose | Want… | Choose | |---|---| | Hard, predictable memory bound; minimal overhead/GC | **ArrayBlockingQueue** | | Maximum throughput with concurrent producers AND consumers | **LinkedBlockingQueue** (two locks) | | Genuinely unbounded buffer | **LinkedBlockingQueue** (default) — but beware OOM | | Fair (FIFO) waiter ordering | **ArrayBlockingQueue** (fair=true) | ## The unbounded trap The biggest practical gotcha: `new LinkedBlockingQueue<>()` is unbounded. If producers are faster than consumers, the queue grows without limit until the JVM throws `OutOfMemoryError`. This is also why `Executors.newFixedThreadPool` (which uses an unbounded LinkedBlockingQueue) can hide a backlog that eventually OOMs. For production backpressure, give it an explicit capacity or use ArrayBlockingQueue. ## Summary ArrayBlockingQueue = bounded array, single lock, low overhead, predictable memory, optional fairness. LinkedBlockingQueue = linked nodes, optionally bounded, dual-lock for parallel put/take throughput, more GC. Reach for ArrayBlockingQueue when bounds and footprint matter; LinkedBlockingQueue when throughput or an unbounded buffer is the goal.

  • Why can LinkedBlockingQueue achieve higher throughput than ArrayBlockingQueue under contention?
    It uses two separate locks (putLock and takeLock) so a producer at the tail and a consumer at the head can operate concurrently, whereas ArrayBlockingQueue's single lock serializes puts and takes.
  • What's risky about Executors.newFixedThreadPool's queue?
    It backs the pool with an unbounded LinkedBlockingQueue, so a flood of submitted tasks queues without limit and can exhaust memory instead of being rejected.

saying these in an interview costs you the question

  • Saying LinkedBlockingQueue is always unbounded — it can take a capacity
  • Claiming both use a single lock — Linked uses two
  • Forgetting the OOM risk of the default unbounded LinkedBlockingQueue
  • Assuming ArrayBlockingQueue can grow its capacity

context