Back to Blog
Java

Java ArrayBlockingQueue: Bounded Queue for Concurrency

java arrayblockingqueue: Learn how to use Java ArrayBlockingQueue for thread-safe producer-consumer patterns, including capacity, fairness, and performance tradeoffs.

Java ConcurrencyBlockingQueueProducer-ConsumerThread SafetyBounded Queue
Illustration of a bounded circular queue with producer and consumer threads accessing an ArrayBlockingQueue, representing Java concurrency.

java arrayblockingqueue requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Java's ArrayBlockingQueue is a bounded, thread-safe queue backed by a fixed-size array. It is a common choice for producer-consumer patterns where the buffer size must be explicitly controlled. Because the capacity is set at construction time and never changes, the queue can reject or block additional elements when full, and it can block consumers when empty. This makes it a predictable building block for coordinating work between threads.

ArrayBlockingQueue at a Glance

ArrayBlockingQueue implements the BlockingQueue interface, which defines methods that wait for space or elements to become available. The queue uses a circular array internally, so elements are stored contiguously and accessed in FIFO order. It does not permit null elements; attempting to add null throws NullPointerException. This restriction is intentional because null is used as a sentinel value in some queue methods, such as poll with a timeout, which returns null when the operation times out.

import java.util.concurrent.ArrayBlockingQueue; ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);

The constructor requires an initial capacity. Optionally, you can pass a fairness flag: new ArrayBlockingQueue<>(10, true). Fairness affects the order in which blocked threads are granted access. With fairness enabled, threads are served in FIFO order, reducing the chance of starvation but adding overhead. With fairness disabled (the default), throughput may be higher, but some threads could wait longer.

Capacity and Fairness

The capacity is fixed at creation. There is no way to resize the queue after it is built. This is a deliberate design choice: the backing array is allocated once, and the queue uses head and tail indices to manage the circular buffer. The fixed capacity is useful when you need to bound memory usage or enforce backpressure in a system. For example, a producer should not be allowed to create an unbounded backlog of tasks.

Fairness is a tradeoff between predictability and throughput. When fairness is enabled, the lock is released and reacquired in a way that gives waiting threads a chance in order of arrival. This can be important if you have many threads competing for the queue and you need to avoid thread starvation. However, the extra bookkeeping can reduce overall throughput. For most applications, the default unfair mode is acceptable, but if you observe uneven processing times or thread starvation, consider enabling fairness.

Blocking Operations: put, take, offer, poll

The core blocking methods are put and take. put adds an element, waiting if the queue is full. take retrieves and removes the head element, waiting if the queue is empty. Both methods throw InterruptedException if the waiting thread is interrupted, which is important for responsive shutdown.

// Producer thread try { queue.put("task"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // restore flag } // Consumer thread try { String task = queue.take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }

For non-blocking or timed operations, offer and poll are available. offer returns false immediately if the queue is full, and poll returns null immediately if the queue is empty. Timed versions, offer(e, timeout, unit) and poll(timeout, unit), wait up to a specified duration. These are useful when you want to avoid indefinite blocking or when you need to periodically check for cancellation.

Producer-Consumer Example

A typical producer-consumer setup uses a shared ArrayBlockingQueue and one or more producer and consumer threads. Here is a minimal example that demonstrates the core pattern.

import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class ProducerConsumerExample { public static void main(String[] args) { BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5); Runnable producer = () -> { try { for (int i = 0; i < 20; i++) { queue.put(i); System.out.println("Produced: " + i); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; Runnable consumer = () -> { try { for (int i = 0; i < 20; i++) { Integer value = queue.take(); System.out.println("Consumed: " + value); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; Thread producerThread = new Thread(producer); Thread consumerThread = new Thread(consumer); producerThread.start(); consumerThread.start(); } }

The queue holds at most five integers. When the producer tries to put a sixth element, it blocks until the consumer takes one. This backpressure prevents the producer from running too far ahead. In a real application, you would likely have multiple producers and consumers, and you would use a shutdown mechanism such as a poison pill or a volatile flag to stop the threads cleanly.

Performance and Concurrency Considerations

ArrayBlockingQueue uses a single lock to protect both enqueue and dequeue operations. This is simpler than some other queues, but it means that producers and consumers contend for the same lock. In high-throughput scenarios, this can become a bottleneck. LinkedBlockingQueue uses two locks (one for put, one for take) and can offer better throughput when producers and consumers are on different threads, at the cost of slightly more complex implementation.

Memory usage is another factor. ArrayBlockingQueue pre-allocates the backing array, so it uses a fixed amount of memory regardless of how many elements are actually present. LinkedBlockingQueue allocates nodes dynamically, which can be more memory-efficient when the queue is often near empty, but it also creates more garbage. If you need predictable memory usage, ArrayBlockingQueue is the safer choice.

The fairness flag also affects performance. Fair mode requires more synchronization to maintain FIFO ordering, which can reduce throughput. If you do not have a specific starvation problem, the default unfair mode is usually preferable.

Common Pitfalls and Misuse

One common mistake is forgetting to handle InterruptedException. When a thread is blocked on put or take, interrupting it causes the method to throw InterruptedException. If you catch it and do not restore the interrupt flag, the thread's interrupted status is lost, which can break shutdown logic. Always re-interrupt the current thread after catching InterruptedException unless you have a specific reason to swallow it.

Another pitfall is using add or remove on a full or empty queue. The add method throws IllegalStateException when the queue is full, and remove throws NoSuchElementException when empty. These are not blocking operations and are meant for use when you are sure the queue has capacity or elements. If you need a non-blocking but safe operation, use offer and poll.

Also, do not assume that ArrayBlockingQueue is a good fit for unbounded data. The fixed capacity is a feature, but it can cause deadlock if producers and consumers are not balanced. For example, if you have a single producer and a single consumer, and the producer blocks because the queue is full, but the consumer is waiting for more data before it starts processing, you have a deadlock. This is not a fault of the queue itself; it is a design issue in your concurrency logic.

Choosing Between ArrayBlockingQueue and Other Queues

When deciding whether to use ArrayBlockingQueue, consider the requirements for capacity, memory, and concurrency. If you need a bounded buffer with a known maximum size, ArrayBlockingQueue is straightforward. If you need an unbounded queue, LinkedBlockingQueue with a large capacity or ConcurrentLinkedQueue might be more appropriate, but be aware that unbounded queues can lead to memory exhaustion.

For high-throughput scenarios with many producers and consumers, LinkedBlockingQueue's dual-lock design may reduce contention. However, ArrayBlockingQueue often has lower per-operation overhead because it does not allocate nodes. In practice, the difference is usually small unless you are processing millions of items per second. Benchmark your specific workload to make an informed choice.

If you need priority ordering, PriorityBlockingQueue is a better fit, but it is unbounded. If you need a delay-based scheduling, DelayQueue is appropriate. For simple point-to-point handoff without buffering, SynchronousQueue is designed for that. The choice depends on the exact behavior you need.

ArrayBlockingQueue remains a solid default for bounded, thread-safe FIFO communication. Its fixed capacity and single-lock design make it easy to reason about, and its blocking methods integrate cleanly with Java's concurrency model. When you understand the tradeoffs around fairness and contention, you can use it effectively in production systems.

java arrayblockingqueue: Practical Usage and Code Examples | RYUSLOG DEV