Back to Blog
Java

java linkedblockingqueue: Producer-Consumer Implementation

Learn how java linkedblockingqueue works, its capacity behavior, and how to use it in producer-consumer patterns with practical examples.

java concurrencyblocking queueproducer-consumerthread safetyjava collectionsmultithreading
Diagram showing a java LinkedBlockingQueue connecting producer and consumer threads with a bounded buffer.

java linkedblockingqueue is a thread-safe, optionally bounded queue that implements the BlockingQueue interface. It is a common choice for producer-consumer designs where threads need to hand off work without busy-waitning. This article covers its behavior, typical usage, and the tradeoffs you should consider before using it.

What Is java linkedblockingqueue and When to Use It

LinkedBlockingQueue is a linked-node-based queue that can be created with or without a capacity limit. When a capacity is specified, the queue is bounded; otherwise it can grow without bound. It is designed for concurrent access: all mutating operations are thread-safe, and blocking operations like put and take coordinate producer and consumer threads efficiently.

Use it when you need a FIFO queue that supports multiple producers and consumers without external synchronization. The class handles locking internally, so your code does not need to guard queue access with synchronized blocks or explicit locks. This makes it a practical building block for work queues, event pipelines, and task distribution.

Core Behavior: Capacity and Blocking Semantics

The queue's behavior depends on whether it is bounded or unbounded. An unbounded LinkedBlockingQueue never blocks on put because there is always room for a new element. A bounded queue, created by passing a capacity to the constructor, will block a producer thread that calls put when the queue is full, until space becomes available.

Similarly, take blocks when the queue is empty, waiting for an element to appear. This is the core of producer-consumer coordination: producers and consumers do not need to know about each other's state; they simply interact with the queue.

The default constructor creates an unbounded queue with a capacity of Integer.MAX_VALUE. While that sounds convenient, an unbounded queue can consume all available memory if producers outpace consumers. In production, prefer a bounded queue with a defined capacity to apply backpressure.

Putting and Taking Elements: put, take, offer, poll

The BlockingQueue interface defines several methods for adding and removing elements. The blocking methods put and take wait indefinitely until the operation succeeds. The non-blocking methods offer and poll return a special value (false or null) when the queue is full or empty, respectively. There are also timed versions of offer and poll that wait up to a specified timeout.

import java.util.concurrent.LinkedBlockingQueue; LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(10); // Blocking put queue.put("task-1"); // Blocking take String task = queue.take(); // Non-blocking offer boolean accepted = queue.offer("task-2"); // Non-blocking poll String maybeTask = queue.poll();

Use put and take when you want the thread to wait until the operation is possible. Use offer and poll when you need to avoid indefinite blocking, for example when implementing a non-blocking shutdown or when you want to handle a full queue differently.

Producer-Consumer Example with LinkedBlockingQueue

The most common pattern is a fixed number of producer threads adding work items and a fixed number of consumer threads processing them. The queue decouples the two groups, allowing them to operate at different speeds.

import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ProducerConsumerExample { public static void main(String[] args) { LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(5); ExecutorService producers = Executors.newFixedThreadPool(2); ExecutorService consumers = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { final int id = i; producers.submit(() -> { try { queue.put("task-" + id); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } for (int i = 0; i < 3; i++) { consumers.submit(() -> { try { while (true) { String task = queue.take(); System.out.println("Processing " + task); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } producers.shutdown(); // In a real application, you would also shutdown consumers after work is done. } }

This example uses a bounded queue of size 5. Producers block when the queue is full, which naturally limits how far ahead they can get. Consumers block when the queue is empty, waiting for new work. The InterruptedException handling is essential: when a thread is blocked on put or take and is interrupted, the method throws InterruptedException. Restoring the interrupt flag with Thread.currentThread().interrupt() is a good practice so that higher-level code can observe the interruption.

LinkedBlockingQueue vs ArrayBlockingQueue: When the Difference Matters

Both LinkedBlockingQueue and ArrayBlockingQueue implement BlockingQueue and are thread-safe. The key difference is the underlying data structure. ArrayBlockingQueue uses a fixed circular array, while LinkedBlockingQueue uses linked nodes. This leads to different tradeoffs.

FeatureLinkedBlockingQueueArrayBlockingQueue
CapacityOptional (bounded or unbounded)Required (always bounded)
Memory allocationAllocates a node per elementPre-allocates array
LockingUses two locks (head and tail)Uses a single lock
Throughput under contentionOften better with high concurrencyMay be simpler for small queues
Iteration orderFIFOFIFO

Use ArrayBlockingQueue when you need a fixed, predictable capacity and want to avoid per-element allocation. Use LinkedBlockingQueue when you need an unbounded queue or when the higher concurrency of separate head and tail locks matters for your workload. The difference is rarely decisive for small queues; measure with your actual load if performance is critical.

Performance and Memory Considerations in Practice

LinkedBlockingQueue's two-lock design allows put and take operations to proceed concurrently in many cases, which can reduce contention compared to a single-lock queue. However, this comes at the cost of more complex internal coordination and per-node allocation. For a bounded queue, the node objects are created as elements are added and garbage-collected as they are removed. If you are enqueuing and dequeuing millions of small objects, this allocation overhead can become noticeable.

An unbounded queue can cause memory exhaustion if producers run faster than consumers for a sustained period. Always prefer a bounded queue in production to enforce backpressure. The capacity should be chosen based on the acceptable latency and throughput of your system. A larger queue allows producers to run ahead but increases memory usage and the time a task may wait before being processed.

There are no guarantees about fairness or ordering beyond FIFO for the queue itself. If you need strict ordering of tasks based on priority, consider a PriorityBlockingQueue instead. For most work queues, FIFO is the natural choice.

Common Pitfalls and How to Avoid Them

One common mistake is using offer and poll without checking the return value. If you ignore the boolean or null result, you may silently lose tasks or attempt to process a null task. Always handle the failure case explicitly.

Another pitfall is forgetting to handle InterruptedException correctly. Blocking methods throw this exception when the thread is interrupted. Simply catching and ignoring it can leave the thread in an interrupted state, causing subsequent blocking calls to fail immediately. Always restore the interrupt flag or propagate the exception appropriately.

A third issue is shutting down a producer-consumer system. If you shut down the executor while consumers are still blocked on take, they will wait forever unless you interrupt them or use a poison-pill approach. A common pattern is to put a special sentinel object on the queue to signal consumers to exit. For example, with a String queue, you might put "POISON" and have consumers check for it.

String task = queue.take(); if ("POISON".equals(task)) { break; }

This works, but be careful that each consumer receives its own poison pill if you have multiple consumers. Alternatively, use poll(timeout, unit) to periodically check a shutdown flag, or interrupt the consumer threads directly.

Finally, do not assume that LinkedBlockingQueue preserves insertion order when multiple producers are involved. It does preserve FIFO order for the sequence of successful put calls, but the interleaving of calls from different threads is determined by the runtime scheduler. If you need a global ordering across producers, you must coordinate at a higher level.

java linkedblockingqueue: Usage and Producer-Consumer Patter | RYUSLOG DEV