Blocking and concurrent queues at a glance
BlockingQueue, concurrent queues and queue-selection trade-offs: bounded buffers, hand-offs, priority and delay queues, transfer queues, poison pills and back-pressure.
On this page
🚦 What are blocking and concurrent queues?
Blocking and concurrent queues are queue implementations designed for threads sharing work safely. A BlockingQueue can make a producer wait when the buffer is full and make a consumer wait when the buffer is empty. A ConcurrentLinkedQueue is different: it is thread-safe and non-blocking, so a failed poll() returns null instead of waiting.
Think of them as different loading docks. Some docks have a bounded waiting area and make trucks wait outside when it fills. Some docks are pure hand-offs with no storage at all. Some docks never block, they just let workers check whether a package is available and move on.
What you’ll learn
- Choose between blocking, non-blocking, bounded, priority, delay and hand-off queues
- Explain the four
BlockingQueuemethod families: throwing, special value, blocking and timed - Use a bounded
ArrayBlockingQueuefor producer-consumer back-pressure - Avoid the **unbounded
LinkedBlockingQueuetrap** in fixed thread pools - Know what
SynchronousQueue,DelayQueue,PriorityBlockingQueueandTransferQueueare for - Recognize
ConcurrentLinkedQueueandConcurrentLinkedDequeas lock-free non-blocking queues wheresize()is O(n)
For this lesson, we'll map the queue family, not rebuild the concurrency machinery underneath it. We'll focus on BlockingQueue method families, ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue, DelayQueue, SynchronousQueue, TransferQueue, ConcurrentLinkedQueue, ConcurrentLinkedDeque, LinkedBlockingDeque, back-pressure and the producer-consumer poison-pill pattern. Deeper lock, condition, CAS and memory-model internals are covered in the Concurrency module.
Below the line (out of scope)
- Single-threaded queues and deques: Queue, Deque and ArrayDeque
- Priority ordering mechanics: PriorityQueue and binary heaps
- Concurrent maps: Hashtable, synchronizedMap and the concurrent maps
- Thread pools, locks, conditions, atomics and happens-before: Concurrency module
The 30-second answer
Use a BlockingQueue when producers and consumers should coordinate through waiting. Its four method families are: throw (add, remove, element), special value (offer, poll, peek), block forever (put, take) and timed wait (offer(e, timeout, unit), poll(timeout, unit)). Start with a bounded ArrayBlockingQueue for a normal producer-consumer buffer because it gives clear back-pressure. Use LinkedBlockingQueue only with an explicit capacity unless you really mean Integer.MAX_VALUE, which is the trap behind many fixed-thread-pool memory incidents. Use SynchronousQueue for direct hand-off, PriorityBlockingQueue for concurrent priority work, DelayQueue for delayed tasks, LinkedTransferQueue when producers may wait for consumers, and ConcurrentLinkedQueue or ConcurrentLinkedDeque when you want lock-free non-blocking access and can handle null on empty.
Interview radar
Asked often: Which collection should I use when producers must wait if the queue is full?
Senior rounds probe: LinkedBlockingQueue default capacity, newFixedThreadPool's unbounded queue, SynchronousQueue in cached pools, and why ConcurrentLinkedQueue.size() is O(n).
Production angle: Back-pressure is a safety mechanism. An unbounded queue turns latency into memory growth until the JVM falls over.
01Picture it first: pick the queue by the kind of waiting
Start with the question, not the class name: should a thread wait, return immediately, or hand work directly to another thread? Once you answer that, the family becomes much less memorisation-heavy.
The decision tree below is the map I use in code reviews.
A decision tree starts with whether threads need to wait, then branches to BlockingQueue choices such as bounded FIFO, linked FIFO, priority, delay and hand-off queues, or to non-blocking lock-free FIFO queues
Notice the first fork. BlockingQueue is about coordination through waiting. ConcurrentLinkedQueue is about safe concurrent access without waiting. Both are thread-safe, but they solve different problems.
02BlockingQueue under the hood: four ways to ask
BlockingQueue is the interface behind the classic producer-consumer pattern. It extends Queue, but adds methods that know how to wait for capacity or wait for data.
Try it yourself first
Before reading the table, decide which method you would call if a producer should wait up to 200 milliseconds for space and then give up.
| Family | Insert | Remove | Examine | What happens on full or empty |
|---|---|---|---|---|
| Throws exception | add(e) |
remove() |
element() |
throws IllegalStateException or NoSuchElementException |
| Special value | offer(e) |
poll() |
peek() |
returns false or null |
| Blocks | put(e) |
take() |
not available | waits until possible |
| Times out | offer(e, time, unit) |
poll(time, unit) |
not available | waits up to the timeout |
The answer to the prompt is offer(item, 200, TimeUnit.MILLISECONDS). That method is useful when you want back-pressure, but you also want a request thread to fail or shed load instead of waiting forever.
Here is the minimum producer-consumer shape:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
record OrderJob(String id, int amountCents) {}
BlockingQueue<OrderJob> queue = new ArrayBlockingQueue<>(100);
boolean accepted = queue.offer(new OrderJob("o-42", 49900), 200, TimeUnit.MILLISECONDS);
if (!accepted) {
throw new IllegalStateException("too many orders queued");
}
OrderJob next = queue.take(); // waits while the queue is empty
Notice three things:
putandtakeare coordination points. They block the calling thread.offerwith a timeout is often better at service boundaries because it gives you a limit.BlockingQueuerejectsnull, sonullcan safely mean "nothing available" forpoll.
Two producers put jobs into a bounded blocking queue while two consumers take jobs out; when the buffer is full producers wait, creating back-pressure
Back-pressure is the important production word here. If consumers can process 200 jobs per second and producers send 1,000 jobs per second, a queue does not create capacity. A bounded queue makes the overload visible by slowing or rejecting producers. An unbounded queue hides the overload as growing memory and longer latency.
The most common queue incident I've seen is not a race condition. It is a fixed worker pool with an unbounded queue. Traffic spikes, workers keep processing at the same rate, the queue grows for 20 minutes, and then the service dies with OutOfMemoryError. A capacity limit would have forced the caller to slow down or fail earlier, when the system was still healthy enough to explain itself.
03The BlockingQueue family at a glance
Let's name the pieces first, then compare them. You do not need every internal lock path for a Collections interview, but you should know what each queue is for.
ArrayBlockingQueue: bounded array, one lock
ArrayBlockingQueue is a fixed-size circular array. You choose the capacity in the constructor, and it never grows. Internally it uses one lock with two conditions: one for "not empty" and one for "not full".
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Concurrent non-blocking queues
- Producer-consumer with poison pill shutdown
- Which one should you choose?
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal
Further reading (11)
- BlockingQueue (Java SE 21 API)
- ArrayBlockingQueue (Java SE 21 API)
- LinkedBlockingQueue (Java SE 21 API)
- SynchronousQueue (Java SE 21 API)
- DelayQueue (Java SE 21 API)
- PriorityBlockingQueue (Java SE 21 API)
- TransferQueue and LinkedTransferQueue (Java SE 21 API)
- ConcurrentLinkedQueue (Java SE 21 API)
- Executors (Java SE 21 API)
- Baeldung: Guide to java.util.concurrent.BlockingQueue
- Baeldung: A Guide to Concurrent Queues in Java