Definition
- ArrayBlockingQueue – fixed-size array, ONE ReentrantLock for both put and take, optional fairness (FIFO for waiting threads).
- LinkedBlockingQueue – linked nodes, TWO separate locks (putLock + takeLock). Put and take can run concurrently, giving higher throughput. Can be bounded or unbounded.
- PriorityBlockingQueue – unbounded min-heap. Elements ordered by Comparator or Comparable. poll() returns the smallest (highest priority), NOT FIFO. Only blocks on take() when empty.
- SynchronousQueue – ZERO internal capacity. put() blocks until a consumer calls take(). take() blocks until a producer calls put(). Direct handoff with no buffering. Used by newCachedThreadPool().
- DelayQueue – elements implement Delayed interface. take() blocks until the element's delay expires. Used for scheduled execution, retry with backoff, cache expiration.
ArrayBlockingQueue is a fixed-size bench at National Bank of India with one guard at the door. LinkedBlockingQueue is an expanding rope line with two guards: one at the entrance and one at the exit, so customers can enter and exit simultaneously. PriorityBlockingQueue is a VIP line where the highest-priority customer is served first, not the one who arrived first. SynchronousQueue is a direct counter: Rajesh hands his form directly to the teller with no waiting area. DelayQueue is an appointment system: Priya's request is only processed when her scheduled time arrives.
Understanding the Concepts
What are the five BlockingQueue implementations?
Java provides five standard BlockingQueue implementations, each designed for a specific concurrency use case.
package com.bank.concurrency;
import java.util.concurrent.*;
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// ArrayBlockingQueue: fixed capacity, one lock, optional fairness
BlockingQueue<String> arrayBQ = new ArrayBlockingQueue<>(100, true);
// LinkedBlockingQueue: two locks, higher throughput
BlockingQueue<String> linkedBQ = new LinkedBlockingQueue<>(1000);
// PriorityBlockingQueue: heap-ordered, unbounded
BlockingQueue<String> priorityBQ = new PriorityBlockingQueue<>();
// SynchronousQueue: zero capacity, direct handoff
BlockingQueue<String> syncQ = new SynchronousQueue<>();
// DelayQueue: time-based release
DelayQueue<DelayedTask> delayQ = new DelayQueue<>();
}
}
How are they different?
| Queue | Capacity | Locks | Order | Blocks on | Use case |
|---|---|---|---|---|---|
| ArrayBlockingQueue | Fixed | 1 | FIFO | put + take | Bounded buffer, fairness required |
| LinkedBlockingQueue | Bounded/unbounded | 2 | FIFO | put + take | Producer-consumer, ThreadPool default |
| PriorityBlockingQueue | Unbounded | 1 | Priority | take only | Priority processing |
| SynchronousQueue | 0 | N/A | Handoff | put + take | Direct handoff, CachedPool |
| DelayQueue | Unbounded | 1 | Delay | take only | Scheduled tasks, retry |
Why does LinkedBlockingQueue have higher throughput?
LinkedBlockingQueue uses two separate locks: a putLock for producers and a takeLock for consumers. This means put() and take() can run concurrently on different threads without contending for the same lock. ArrayBlockingQueue uses a single ReentrantLock for both operations, so producers and consumers serialize on the same lock.
// LinkedBlockingQueue internal structure (simplified):
// putLock -> protects the tail of the linked list
// takeLock -> protects the head of the linked list
// Producer adds at tail (putLock), Consumer removes from head (takeLock)
// Both can proceed simultaneously!
// ArrayBlockingQueue internal structure (simplified):
// Single lock -> protects the entire array
// Producer and consumer must take turns
When would you use each one in a banking system?
At National Bank of India, different queues serve different purposes.
package com.bank.queues;
import java.math.BigDecimal;
import java.util.concurrent.*;
public class BankingQueueExamples {
// Settlement queue: bounded, high-throughput producer-consumer
private final BlockingQueue<Settlement> settlementQueue =
new LinkedBlockingQueue<>(10_000);
// VIP trade queue: highest-value trades processed first
private final BlockingQueue<Trade> vipQueue =
new PriorityBlockingQueue<>(100,
(a, b) -> b.getValue().compareTo(a.getValue())); // highest value first
// Retry queue: failed payments retried after a 30-second delay
private final DelayQueue<RetryablePayment> retryQueue = new DelayQueue<>();
}
How does SynchronousQueue work as direct handoff?
SynchronousQueue has zero internal capacity. put() blocks until a consumer calls take(), and take() blocks until a producer calls put(). This provides direct handoff with no buffering. It is used internally by Executors.newCachedThreadPool() because the pool needs a new thread for every task that arrives when no idle thread is available.
How does DelayQueue enforce time-based release?
Elements must implement the Delayed interface. take() blocks until the element's delay has expired. Elements are ordered by expiry time.
package com.bank.retry;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class RetryablePayment implements Delayed {
private final String paymentId;
private final long retryAt; // epoch millis
public RetryablePayment(String paymentId, long delayMs) {
this.paymentId = paymentId;
this.retryAt = System.currentTimeMillis() + delayMs;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(retryAt - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed other) {
return Long.compare(this.retryAt, ((RetryablePayment) other).retryAt);
}
}
What are common mistakes?
Using an unbounded LinkedBlockingQueue (no capacity specified) with a ThreadPoolExecutor hides pool saturation. The queue grows without limit, consuming memory, and tasks wait indefinitely instead of being rejected. Always specify a capacity.
// WRONG: unbounded queue hides saturation
new LinkedBlockingQueue<>() // grows to Integer.MAX_VALUE
// CORRECT: bounded queue with rejection policy
new LinkedBlockingQueue<>(1000)
What are common interview follow-up questions?
- Is PriorityBlockingQueue thread-safe? Yes, but its iterator does NOT guarantee any particular order. Only poll() and take() guarantee priority order.
- Can SynchronousQueue hold even one element? No. It has zero capacity. Every put() must rendezvous with a take().
- What queue does ThreadPoolExecutor use by default? It depends on how you create it. Executors.newFixedThreadPool uses unbounded LinkedBlockingQueue. Executors.newCachedThreadPool uses SynchronousQueue.
Visual Overview
BlockingQueue Implementations ═════════════════════════════ ArrayBQ(100): [1][2][3]...[100] ← fixed array, 1 lock LinkedBQ(1000): 1→2→3→...→1000 ← linked nodes, 2 locks PriorityBQ: heap: min at root ← ordered by priority SynchronousQ: [ ] ← zero capacity, direct handoff DelayQ: [t+5s][t+10s] ← released when delay expires
Key Takeaway
Five BlockingQueue implementations serve different concurrency patterns. LinkedBlockingQueue's two-lock design (separate putLock and takeLock) enables concurrent production and consumption, making it the highest-throughput option and the default choice for ThreadPoolExecutor. ArrayBlockingQueue provides fairness but uses a single lock, so producers and consumers serialize. PriorityBlockingQueue orders elements by priority rather than FIFO. SynchronousQueue provides zero-capacity direct handoff for thread-per-task patterns. DelayQueue releases elements only after their delay expires.
At National Bank of India, the settlement system used LinkedBlockingQueue for throughput, the VIP trade queue used PriorityBlockingQueue to process high-value trades first, and the retry queue used DelayQueue with a 30-second backoff between attempts.
Remember one sentence: LinkedBlockingQueue's two-lock design gives the highest throughput for producer-consumer, while SynchronousQueue provides zero-capacity direct handoff and PriorityBlockingQueue orders by priority.
---