Introduction
The Java LinkedBlockingQueue class was added in Java 5 (2004) as part of JSR 166. It is an optionally-bounded blocking queue backed by a linked list of Node objects, with separate locks for the head and the tail. The two-lock design allows one producer and one consumer to operate concurrently without contending on the same monitor, which is a substantial throughput advantage over the single-lock ArrayBlockingQueue. The java linkedblockingqueue implementation is the default queue for ThreadPoolExecutor and is the workhorse of every Java logging and event-processing pipeline.
For a Java learner, LinkedBlockingQueue is the right default whenever you need a thread-safe queue that supports blocking operations. The classic mistake is to assume that ArrayBlockingQueue is faster because it uses an array; in practice, LinkedBlockingQueue often outperforms it under contention because the two-lock design keeps the head and the tail operations independent. This post walks through the two-lock algorithm, the node allocation pattern, the bounded and unbounded variants, and the implications for back-pressure.
What is Java LinkedBlockingQueue?
The java linkedblockingqueue class implements the BlockingQueue interface using a singly linked list of Node objects. Each Node holds an item and a volatile reference to the next Node. The queue has two atomic references: head points to a dummy node whose next field points to the first real element, and last points to the tail node. Two ReentrantLocks (putLock and takeLock) protect the head and the tail respectively, and two Conditions (notFull and notEmpty) signal waiting threads.
Conceptually, LinkedBlockingQueue belongs to the two-lock queue family of concurrent data structures, alongside the older LinkedBlockingDeque (which uses the same idea extended to both ends). It is distinct from ArrayBlockingQueue (which uses a single lock and a circular array) and from ConcurrentLinkedQueue (which is non-blocking using CAS on the head and tail). It is also distinct from LinkedTransferQueue, which uses a more sophisticated transfer protocol that can hand items directly from producer to consumer without buffering.
How Java LinkedBlockingQueue Works - Step by Step
Step 1: The internal Node class. Each Node has a volatile E item and a volatile Node next. The head of the queue is a sentinel Node whose item field is null; the first real element is at head.next. The sentinel simplifies the take() code because it makes the empty-queue case look the same as the non-empty case (head.next is just null instead of head itself).
Step 2: The two locks. putLock protects the tail of the queue; takeLock protects the head. Because producers only touch the tail and consumers only touch the head, two threads (one producer, one consumer) can run in parallel without contending. The AtomicInteger count tracks the total number of elements and is used to enforce the capacity bound without holding both locks.
Step 3: put(e) acquires putLock, checks whether the AtomicInteger count equals the capacity, and if so awaits on notFull. When space is available, it allocates a new Node, links it after the current tail, updates last, increments count, and signals notEmpty if the queue transitioned from empty to non-empty. Finally it releases putLock.
Step 4: take() acquires takeLock, checks whether the AtomicInteger count is zero, and if so awaits on notEmpty. When an element is available, it reads head.next, unlinks it from head, updates head to the dequeued node, decrements count, and signals notFull if the queue transitioned from full to non-full. Finally it releases takeLock. The returned item is the item field of the dequeued node, which is then nulled out to help garbage collection.
Step 5: The size() and remainingCapacity() methods read the AtomicInteger count atomically and derive their answers from it. The iterator is weakly consistent: it walks the linked list using volatile reads and may or may not reflect concurrent modifications. The iterator does not throw ConcurrentModificationException.
Java Program for LinkedBlockingQueue
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class LinkedBlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// Optionally-bounded queue with capacity 5
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
AtomicInteger sum = new AtomicInteger();
Runnable producer = () -> {
for (int i = 1; i <= 10; i++) {
try {
queue.put(i);
System.out.println("put " + i);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return;
}
}
};
Runnable consumer = () -> {
try {
for (int i = 0; i < 10; i++) {
Integer v = queue.take();
sum.addAndGet(v);
System.out.println(" took " + v);
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
};
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(producer);
pool.submit(consumer);
pool.shutdown();
pool.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("sum of 1..10 = " + sum.get());
System.out.println("queue size = " + queue.size());
}
}
Code Explanation - Line by Line
The main method constructs a LinkedBlockingQueue with capacity 5. The bounded capacity is essential for demonstrating back-pressure: the producer will block on the sixth put because the consumer has not yet drained enough. Without the bound, the producer would dump all 10 items in microseconds and the demo would not show any blocking.
The producer Runnable loops from 1 to 10 inclusive, calling queue.put(i) on each iteration. The put method blocks when the queue is full, which is exactly the behaviour we want. The InterruptedException catch follows the canonical pattern: restore the interrupt flag and return early, so the caller up the stack can detect the cancellation.
The consumer Runnable mirrors the producer: it loops 10 times calling queue.take(). The take method blocks when the queue is empty. The returned Integer is added to an AtomicInteger sum, which will end up at 55 (the sum of 1 through 10) if everything works correctly.
The thread pool spins up the two Runnables on separate threads. The shutdown and awaitTermination calls block the main thread until both tasks finish. The 10-second timeout is generous for a workload that should finish in milliseconds, but it guards against hangs if a thread dies unexpectedly.
The final print statements report the sum and the queue size. The sum should be 55 (1 plus 2 plus 3 plus 4 plus 5 plus 6 plus 7 plus 8 plus 9 plus 10 equals 55), and the queue size should be 0 because the consumer drained every item. If the consumer had been cancelled early, the sum would be less than 55 and the queue size would show the leftover items.
Sample Input and Output
Input:
Bounded LinkedBlockingQueue capacity = 5
Producer: 10 items via put()
Consumer: 10 items via take()
Output (partial, interleaving varies):
put 1
put 2
put 3
put 4
put 5
took 1
put 6
took 2
put 7
took 3
...
put 10
took 10
sum of 1..10 = 55
queue size = 0
The interleaving depends on the OS thread scheduler. The producer can put up to 5 items before the consumer starts; on the sixth put, the producer blocks until the consumer takes one. Both the sum (55) and the final queue size (0) confirm that every item produced was eventually consumed, and that no item was lost or duplicated in the hand-off.
Common Mistakes to Avoid
Mistake 1: Constructing LinkedBlockingQueue with the no-arg constructor and expecting back-pressure. The no-arg constructor uses Integer.MAX_VALUE as the capacity, which means the queue is effectively unbounded. A fast producer will blow up the heap before the consumer catches up. Always pass an explicit capacity if you want throttling.
Mistake 2: Using peek() to check for emptiness before calling take(). The peek() method returns null when the queue is empty, but a null peek does not guarantee that a subsequent take() will block briefly; between the peek and the take, another consumer may drain the only available item. Use take() directly and let it block, or use poll(timeout, unit) for a bounded wait.
Mistake 3: Calling size() in a hot path. The size is O(1) because it just reads the AtomicInteger count, but the count is updated on every put and take; reading it is cheap but the contention on the AtomicInteger itself can be measurable in micro-benchmarks. Use isEmpty() when you only need the boolean.
Mistake 4: Sharing a single LinkedBlockingQueue across more than one producer and one consumer and expecting maximum throughput. The two-lock design lets one producer and one consumer run in parallel, but with multiple producers they contend on putLock; with multiple consumers they contend on takeLock. For high-fanout pipelines, consider LinkedTransferQueue which has a more sophisticated multi-producer, multi-consumer algorithm.
Real-World Applications
LinkedBlockingQueue is the default work queue for ThreadPoolExecutor when you call Executors.newFixedThreadPool or Executors.newSingleThreadExecutor. The unbounded variant allows task submission to never block, which is the right default for fixed-thread pools where you want the application to keep moving even if the pool is overwhelmed (at the cost of unbounded heap growth in pathological cases).
Logging frameworks. Log4j 2's AsyncAppender uses a LinkedBlockingQueue by default to ship log events from application threads to a single writer thread. The default capacity is large enough to absorb bursts but bounded to protect against runaway producers. The same pattern appears in Logback's AsyncAppender and in SLF4J's test helpers.
Stream and event processing. Apache Camel uses BlockingQueue for in-JVM messaging between routes. RxJava and Project Reactor use LinkedBlockingQueue as the default buffer between an asynchronous source and a synchronous consumer. The bounded variant is preferred when you want back-pressure that propagates upstream.
Batch processing pipelines. Hadoop's MapReduce uses LinkedBlockingQueue for the in-JVM shuffle buffer between map output and reduce input. Spark's block manager uses LinkedBlockingQueue for the in-JVM transfer between map-side and reduce-side stages. The bounded capacity ensures that a slow reduce stage throttles a fast map stage instead of blowing up the heap.
Conclusion
The java linkedblockingqueue class is an optionally-bounded blocking queue backed by a singly linked list of Node objects, with two ReentrantLocks (putLock and takeLock) for the head and the tail. The two-lock design lets one producer and one consumer run in parallel. To go further, study ArrayBlockingQueue for the bounded single-lock variant and LinkedTransferQueue for the lock-free multi-producer, multi-consumer variant.
Comments
Comments
Post a Comment