Introduction
The Java ArrayBlockingQueue class was added in Java 5 (2004) as part of JSR 166. It is a bounded blocking queue backed by a single circular array of Object references, with a single ReentrantLock protecting all reads and writes. The class is the canonical example of a bounded buffer in Java: every put() blocks when the array is full, every take() blocks when the array is empty, and the lock serialises every operation. The java arrayblockingqueue implementation is the standard choice when you need back-pressure with a fixed memory footprint, and it is the textbook example used to teach the producer-consumer pattern.
For a Java learner, ArrayBlockingQueue is the right tool when you need a bounded queue with predictable memory usage and you cannot afford the per-element Node allocation that LinkedBlockingQueue pays. The classic trade-off is that ArrayBlockingQueue uses one lock, so producers and consumers contend on it, while LinkedBlockingQueue uses two locks, so a single producer and a single consumer can run in parallel. Pick ArrayBlockingQueue when the producer and consumer counts are similar and the throughput penalty of the single lock is acceptable; pick LinkedBlockingQueue when you need the parallelism. This post walks through the circular array, the fair versus non-fair lock choice, and a working producer-consumer demo.
What is Java ArrayBlockingQueue?
The java arrayblockingqueue class implements the BlockingQueue interface using a fixed-size Object array of length equal to the capacity passed at construction. Two integer indices, putIndex and takeIndex, point to the next slot to write and the next slot to read. An integer count tracks the number of elements currently in the queue. A single ReentrantLock (with two Conditions, notFull and notEmpty) protects all the state, which means that any two operations (even a put and a take from different threads) must serialise on the lock.
Conceptually, ArrayBlockingQueue belongs to the bounded-buffer family of concurrent data structures, alongside the historical BoundedBuffer examples from concurrent-programming textbooks. It is distinct from LinkedBlockingQueue (which uses a linked list and two locks) and from SynchronousQueue (which has zero capacity and hands items directly). It is also distinct from PriorityBlockingQueue (which uses a heap rather than FIFO order). The "fair" constructor parameter controls whether the ReentrantLock is fair (FIFO among waiting threads) or non-fair (the default, which allows barging for higher throughput).
How Java ArrayBlockingQueue Works - Step by Step
Step 1: The circular array. The internal items array has a fixed length equal to the capacity. The putIndex is incremented modulo capacity after each insert; the takeIndex is incremented modulo capacity after each removal. This circular structure means the array never has to be resized; the capacity is fixed at construction time, which is why the queue can be used as a fixed-memory back-pressure buffer.
Step 2: The single ReentrantLock. Every mutative operation (put, offer, poll, take, remove) acquires the lock. The lock guards putIndex, takeIndex, count, and the items array. Because there is only one lock, a producer and a consumer cannot operate in parallel; they must serialise on the lock. This is the trade-off for the array's cache locality and the absence of per-element allocation.
Step 3: put(e) acquires the lock, checks whether count equals items.length, and if so awaits on notFull. When space is available, it writes e to items[putIndex], increments putIndex modulo capacity, increments count, and signals notEmpty (in case a consumer is waiting). It then releases the lock. The signal is sent while still holding the lock, which is correct because the Condition's signal method does not require the lock to be released first.
Step 4: take() acquires the lock, checks whether count is zero, and if so awaits on notEmpty. When an element is available, it reads items[takeIndex], nulls out the slot to help garbage collection, increments takeIndex modulo capacity, decrements count, and signals notFull (in case a producer is waiting). It then releases the lock and returns the read element.
Step 5: The fair constructor flag is passed to the ReentrantLock constructor. A fair lock hands the lock to the longest-waiting thread, which prevents thread starvation but adds a small context-switch cost on every acquisition. A non-fair lock (the default) allows the most recently arriving thread to barge ahead, which is faster in practice but can starve a thread under heavy contention. Most applications use the default non-fair lock.
Java Program for ArrayBlockingQueue
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class ArrayBlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// Bounded buffer of capacity 3, fair scheduling
BlockingQueue<String> buffer = new ArrayBlockingQueue<>(3, true);
AtomicInteger produced = new AtomicInteger();
AtomicInteger consumed = new AtomicInteger();
Runnable producer = () -> {
for (int i = 1; i <= 12; i++) {
String item = "msg-" + i;
try {
buffer.put(item);
produced.incrementAndGet();
System.out.println("put " + item);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return;
}
}
};
Runnable consumer = () -> {
for (int i = 0; i < 12; i++) {
try {
String item = buffer.take();
consumed.incrementAndGet();
System.out.println(" got " + item);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return;
}
}
};
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(producer);
pool.submit(consumer);
pool.shutdown();
pool.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("produced = " + produced.get());
System.out.println("consumed = " + consumed.get());
System.out.println("buffer size = " + buffer.size());
}
}
Code Explanation - Line by Line
The main method constructs an ArrayBlockingQueue with capacity 3 and fairness enabled. The small capacity forces frequent blocking on the producer side, which makes the back-pressure visible in the output trace. The fair flag is set to true so that any waiting producer or consumer is served in arrival order, which is the most predictable interleaving for a teaching demo.
The producer Runnable loops 12 times, building a string item and calling buffer.put(item). The put method blocks when the buffer is full (count equals 3), so the producer thread parks until a consumer takes an element. The InterruptedException catch follows the canonical pattern: restore the interrupt flag via Thread.currentThread().interrupt() and return early.
The consumer Runnable mirrors the producer: it loops 12 times calling buffer.take(). The take method blocks when the buffer is empty (count equals 0), so the consumer thread parks until a producer puts an element. The same interrupt handling pattern applies. The two atomic counters (produced and consumed) record how many items were inserted and removed.
The thread pool spins up the two Runnables on separate threads. The shutdown call initiates orderly shutdown, and awaitTermination blocks the main thread until both tasks finish or the 10-second timeout elapses. The 10-second timeout is generous for a workload that should finish in milliseconds.
The final print statements report the produced count, the consumed count, and the buffer size. Both counts should be 12, and the buffer size should be 0 because the consumer drained every item. If the consumer had been cancelled early (for example by a shorter timeout), the produced count would exceed the consumed count, and the buffer size would show the leftover items.
Sample Input and Output
Input:
Bounded ArrayBlockingQueue capacity = 3, fair = true
Producer: 12 items via put()
Consumer: 12 items via take()
Output (partial, interleaving varies):
put msg-1
put msg-2
put msg-3
got msg-1
put msg-4
got msg-2
put msg-5
got msg-3
...
put msg-12
got msg-12
produced = 12
consumed = 12
buffer size = 0
The interleaving depends on the OS thread scheduler and on the fair lock's ordering. The producer can put up to 3 items before it must block on the fourth put until the consumer takes one. Both the produced count (12) and the final buffer size (0) confirm that every item produced was eventually consumed, and that no item was lost or duplicated in the hand-off. The fair lock ensures that no thread is starved even under heavy contention.
Common Mistakes to Avoid
Mistake 1: Constructing ArrayBlockingQueue with capacity zero or one. Capacity zero throws IllegalArgumentException at construction. Capacity one is legal but degenerate; use SynchronousQueue instead for a zero-capacity queue that hands items directly from producer to consumer without buffering.
Mistake 2: Forgetting to handle InterruptedException. The put and take methods declare InterruptedException because they block. The canonical handling is to call Thread.currentThread().interrupt() to restore the interrupt flag and return early, allowing the caller up the stack to detect the cancellation. Swallowing the exception silently is a bug.
Mistake 3: Iterating over the queue while it is being modified. The iterator is weakly consistent, so it does not throw ConcurrentModificationException, but it also does not reflect concurrent modifications reliably. If you need a stable snapshot, drain the queue into a list using drainTo and iterate over that list.
Mistake 4: Enabling the fair flag without measuring the impact. A fair lock prevents starvation but adds a context-switch cost on every acquisition because the lock must wait for the longest-waiting thread even if another thread is ready to grab the lock now. In most applications, the default non-fair lock is faster and the starvation risk is acceptable.
Real-World Applications
ArrayBlockingQueue is the standard choice when you need a bounded buffer with fixed memory and you do not need the per-element allocation overhead of LinkedBlockingQueue. Java's ThreadPoolExecutor accepts an ArrayBlockingQueue as the work queue for cases where you want to bound the number of pending tasks and reject new submissions when the queue is full.
Logging frameworks with strict memory budgets. Embedded and real-time systems that cannot afford unbounded heap growth use ArrayBlockingQueue with a fixed capacity to ship log events from application threads to a single writer thread. The bounded capacity provides back-pressure that prevents the application from overrunning the writer in pathological bursts.
Stream processing pipelines with fixed buffer budgets. Apache Flink and Apache Storm accept ArrayBlockingQueue as the inter-stage transport for stages that need fixed memory. The bounded capacity provides back-pressure that propagates upstream; when the buffer fills, the upstream stage blocks until the downstream stage catches up.
Test harnesses for producer-consumer code. Unit tests for thread-safe code often use ArrayBlockingQueue as the synchroniser between the test thread and the worker thread, with a small capacity to force specific interleavings. The fair flag is frequently enabled in tests to make the interleaving deterministic and reproducible.
Conclusion
The java arrayblockingqueue class is a bounded blocking queue backed by a single circular array of fixed capacity, with a single ReentrantLock (and two Conditions, notFull and notEmpty) protecting all operations. It is the textbook example of a bounded buffer with back-pressure. To go further, study LinkedBlockingQueue for the two-lock variant that allows one producer and one consumer in parallel, and SynchronousQueue for the zero-capacity direct hand-off variant.
Comments
Comments
Post a Comment