All topics
library
advanced

BlockingQueue Implementations

Choose the right BlockingQueue for producer-consumer patterns: ArrayBlockingQueue, LinkedBlockingQueue, or SynchronousQueue.

A BlockingQueue makes the producer-consumer handoff safe without any wait/notify of your own.

BlockingQueue = a sushi conveyor belt. Chefs (producers) put plates on when there's space. Customers (consumers) take plates when available. If the belt is full, chefs wait. If empty, customers wait.

Key Concepts

1
    BlockingQueue<Job> q = new ArrayBlockingQueue<>(100);
2
    // producer
    q.put(job);          // blocks while the queue is full
3
    // consumer
    Job j = q.take();    // blocks while the queue is empty
4
The blocking is the feature. A bounded queue plus put gives you backpressure for free: a producer that outruns the consumers is slowed down instead of filling memory.
put
5
The four method families, and this table is the whole API.
    throws      blocks   times out              returns special
    add(e)      put(e)   offer(e, t, unit)      offer(e) -> false
    remove()    take()   poll(t, unit)          poll()   -> null
    element()   -        -                      peek()   -> null
6
The implementations.
    ArrayBlockingQueue       bounded, one array, a single lock
    LinkedBlockingQueue      optionally bounded, separate put/take locks
    PriorityBlockingQueue    unbounded, ordered by priority
    SynchronousQueue         capacity zero -- a direct handoff
    DelayQueue               take() waits until an element is due
    LinkedTransferQueue      transfer() waits for a consumer
7
LinkedBlockingQueue has two locks, so a producer and a consumer do not contend. That gives higher throughput than ArrayBlockingQueue under load; ArrayBlockingQueue allocates nothing per element and has more predictable latency.
LinkedBlockingQueueArrayBlockingQueue
8
Leave LinkedBlockingQueue unbounded and you have removed the backpressure. It grows until the heap runs out, which is how a slow consumer becomes an OutOfMemoryError far from the real cause. Always pass a capacity.
LinkedBlockingQueueOutOfMemoryError
9
SynchronousQueue holds nothing. Every put waits for a take. That is what makes Executors.newCachedThreadPool() start a new thread per task — the queue can never accept one.
SynchronousQueueputtakeExecutors.newCachedThreadPool()
10
DelayQueue is a scheduler. Elements implement getDelay, and take() returns only expired ones, which is the natural fit for retries and timeouts.
DelayQueuegetDelaytake()
11
And the queue choice shapes a thread pool. Bounded means tasks get rejected under load and the RejectedExecutionHandler decides what happens; unbounded means maximumPoolSize is never reached, because there is always room in the queue.
RejectedExecutionHandlermaximumPoolSize
12
What the interviewer is probing.1. "Why is an unbounded LinkedBlockingQueue risky?" Probing: backpressure. Stalls: "It is flexible." Moves up: it grows until the heap runs out, so a slow consumer becomes an OutOfMemoryError far from the real cause; a capacity gives you backpressure instead.
13
2. "What is the difference between offer, put and add?" Probing: the four method families. Stalls: "They all add." Moves up: add throws when full, offer returns false, offer with a timeout waits, and put blocks indefinitely.
14
3. "Why does a cached thread pool create unbounded threads?" Probing: SynchronousQueue. Stalls: "It is configured that way." Moves up: its queue has zero capacity, so every submission must be handed directly to a thread and the pool starts one whenever none is free.
15
4. "How does queue choice change a ThreadPoolExecutor?" Probing: the interaction people miss. Stalls: "It does not." Moves up: with an unbounded queue maximumPoolSize is never reached, because there is always room; a bounded queue is what makes the pool grow and then reject.