Definition
- Thread-safe FIFO queue backed by linked nodes with optional bounded capacity
- Two-lock design:
putLockfor the tail (producers) andtakeLockfor the head (consumers), allowing simultaneous put and take - Blocking methods:
put(e)blocks if the queue is full (bounded);take()blocks if the queue is empty - Bounded:
new LinkedBlockingQueue<>(1000)– max 1000 elements, provides back-pressure - Unbounded (default): capacity is
Integer.MAX_VALUE– put() never blocks, risking OOM if producers outpace consumers
LinkedBlockingQueue is like a bank's transaction processing line with a capacity limit. If the line holds 1000 transactions and it is full, the next customer waits at the door until a slot opens (put blocks). If the line is empty, the teller waits until a customer arrives (take blocks). The door has two separate locks – one for entering (producers) and one for exiting (consumers) – so entering and exiting happen simultaneously.
Understanding the Concepts
What is LinkedBlockingQueue?
LinkedBlockingQueue is part of java.util.concurrent and is the most commonly used blocking queue in Java. It is designed for producer-consumer scenarios where one or more threads produce items and one or more threads consume them.
The queue provides three types of methods for each operation:
| Operation | Throws exception | Returns special value | Blocks | Times out |
|---|---|---|---|---|
| Insert | add(e) – ISE if full | offer(e) – false if full | put(e) – waits | offer(e, time, unit) |
| Remove | remove() – NSEE if empty | poll() – null if empty | take() – waits | poll(time, unit) |
| Examine | element() – NSEE if empty | peek() – null if empty | N/A | N/A |
package com.bank.service;
import java.util.concurrent.*;
import java.math.BigDecimal;
public class TransactionQueueDemo {
public static void main(String[] args) throws InterruptedException {
// Bounded queue: max 100 pending transactions
BlockingQueue<String> transactionQueue = new LinkedBlockingQueue<>(100);
// Producer thread
Thread producer = new Thread(() -> {
try {
transactionQueue.put("TXN-001: Deposit 50000 to SAV2001");
transactionQueue.put("TXN-002: Withdraw 10000 from CUR3001");
transactionQueue.put("TXN-003: Transfer 25000 SAV2001->CUR3001");
System.out.println("Producer: 3 transactions queued");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
String txn = transactionQueue.take(); // blocks if empty
System.out.println("Consumer processing: " + txn);
Thread.sleep(100); // simulate processing time
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
Producer: 3 transactions queued
Consumer processing: TXN-001: Deposit 50000 to SAV2001
Consumer processing: TXN-002: Withdraw 10000 from CUR3001
Consumer processing: TXN-003: Transfer 25000 SAV2001->CUR3001
Why is the two-lock design important?
LinkedBlockingQueue uses two separate ReentrantLock instances: putLock for the tail and takeLock for the head. This means a producer adding to the tail and a consumer removing from the head can work simultaneously without blocking each other.
In contrast, ArrayBlockingQueue uses a single lock for both operations. Under high contention (many producers and consumers), LinkedBlockingQueue's two-lock design provides significantly higher throughput.
// LinkedBlockingQueue internal (simplified)
private final ReentrantLock putLock = new ReentrantLock();
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final Condition notFull = putLock.newCondition();
private final AtomicInteger count = new AtomicInteger(); // lock-free size
// put() acquires putLock only
// take() acquires takeLock only
// They can execute concurrently!
Why should you always use bounded capacity?
The default constructor new LinkedBlockingQueue<>() creates an unbounded queue with capacity Integer.MAX_VALUE. This is dangerous because put() never blocks – the queue grows without limit. If producers are faster than consumers, the queue consumes all available heap memory and the application crashes with OutOfMemoryError.
Executors.newFixedThreadPool(n) internally uses an unbounded LinkedBlockingQueue. In a banking system processing thousands of transactions per second, this means task submissions never fail – they pile up silently until the JVM runs out of memory.
package com.bank.service;
import java.util.concurrent.*;
public class BoundedVsUnboundedDemo {
public static void main(String[] args) {
// DANGEROUS – unbounded queue
// BlockingQueue<String> unbounded = new LinkedBlockingQueue<>();
// If producer submits 10M tasks and consumer processes 1K/sec,
// queue grows to 10M elements -> OOM!
// SAFE – bounded queue with back-pressure
BlockingQueue<String> bounded = new LinkedBlockingQueue<>(1000);
// When queue reaches 1000, put() blocks the producer
// This is back-pressure: producer slows down to match consumer speed
// SAFE – ThreadPoolExecutor with bounded queue
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // core threads
8, // max threads
60, TimeUnit.SECONDS, // keep-alive
new LinkedBlockingQueue<>(500) // bounded!
);
// When queue is full and max threads are busy,
// RejectedExecutionHandler is invoked (default: AbortPolicy -> throws)
executor.shutdown();
}
}
How is LinkedBlockingQueue different from ArrayBlockingQueue?
| Feature | LinkedBlockingQueue | ArrayBlockingQueue |
|---|---|---|
| Backing structure | Linked nodes | Fixed-size array |
| Locks | Two (put + take) | One (shared) |
| Bounded/Unbounded | Both | Always bounded |
| Fairness option | No | Yes (fair=true) |
| Memory allocation | Per-node (GC pressure) | Pre-allocated array |
| Best for | High throughput | Predictable latency |
// ArrayBlockingQueue – always bounded, single lock, optional fairness
BlockingQueue<String> abq = new ArrayBlockingQueue<>(1000);
// fair=true: longest-waiting thread gets served first (prevents starvation)
BlockingQueue<String> fairAbq = new ArrayBlockingQueue<>(1000, true);
// LinkedBlockingQueue – two locks, higher throughput
BlockingQueue<String> lbq = new LinkedBlockingQueue<>(1000);
What are common mistakes?
Mistake 1: Using unbounded queue in production
// WRONG – unbounded, risks OOM
ExecutorService pool = Executors.newFixedThreadPool(4);
// Internally uses new LinkedBlockingQueue<>() – no capacity limit!
// CORRECT – explicit bounded queue
ThreadPoolExecutor pool = new ThreadPoolExecutor(
4, 4, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);
Mistake 2: Ignoring InterruptedException
// WRONG – swallowing interrupt
try {
String txn = queue.take();
} catch (InterruptedException e) {
// do nothing – BAD! Thread interrupt flag is lost
}
// CORRECT – restore interrupt flag
try {
String txn = queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore flag
return;
}
What should a senior engineer know?
- Memory overhead. Each element in LinkedBlockingQueue is wrapped in a Node object (~40 bytes overhead). For millions of queued items, this matters. ArrayBlockingQueue has no per-element overhead because it uses a pre-allocated array.
- Draining. drainTo(collection, maxElements) atomically removes up to maxElements and adds them to the target collection. This is more efficient than polling in a loop because it acquires the lock once.
- SynchronousQueue. A queue with capacity zero. put() blocks until a consumer calls take(). This is a direct hand-off with no buffering. Used by Executors.newCachedThreadPool().
- Poison pill pattern. To signal consumers to stop, put a special "poison pill" object in the queue. When the consumer takes the poison pill, it exits its processing loop.
What are common interview follow-up questions?
- "What is the difference between put() and offer()?" – put blocks, offer returns false.
- "Why is unbounded queue dangerous?" – producers never slow down, queue grows until OOM.
- "How is LinkedBlockingQueue different from ArrayBlockingQueue?" – two locks vs one, linked nodes vs array.
- "What does Executors.newFixedThreadPool use internally?" – unbounded LinkedBlockingQueue (dangerous).
Visual Overview
LinkedBlockingQueue internal structure
putLock takeLock
↓ ↓
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ TAIL │ ← │ Node │ ← │ Node │ ← │ HEAD │
│(add) │ │ TXN3 │ │ TXN2 │ │ TXN1 │
└──────┘ └──────┘ └──────┘ └──────┘
Producer Consumer
put(e) take()
Two locks → producer and consumer work simultaneously
Bounded queue (capacity=3):
┌──────────────────────────────────┐
│ Queue: [TXN1] [TXN2] [TXN3] │ ← FULL
│ put() BLOCKS (waits for space) │
│ take() returns TXN1, signals put │
└──────────────────────────────────┘
Key Takeaway
LinkedBlockingQueue is the workhorse of Java concurrency. Its two-lock design allows producers and consumers to operate simultaneously, providing high throughput for producer-consumer patterns. put() blocks when full, take() blocks when empty – this automatic coordination eliminates the need for manual synchronization.
The most important rule is to always use bounded capacity. The default unbounded constructor creates a queue that grows without limit, and in a banking system processing thousands of transactions per second, this leads to silent memory exhaustion. Even Executors.newFixedThreadPool() uses an unbounded queue internally, which is why many production systems create their own ThreadPoolExecutor with an explicit bounded queue.
In the National Bank of India's trade processing pipeline, Rajesh uses new LinkedBlockingQueue<>(10000) between the order ingestion service and the settlement engine. When the settlement engine falls behind, the queue fills up and the ingestion service slows down automatically – this is back-pressure, and it prevents the system from crashing under load.
Remember one sentence: LinkedBlockingQueue provides thread-safe blocking FIFO with two locks for concurrent put and take, but always specify a bounded capacity to prevent OutOfMemoryError.
---