How Does Java’s ArrayBlockingQueue Work Under the Hood?
This article explains the internal design of Java’s ArrayBlockingQueue—a fixed‑size, FIFO blocking queue that uses a ReentrantLock and Condition objects to coordinate producers and consumers, detailing its fields, fairness option, and the concrete implementations of add, offer, poll, and take methods.
ArrayBlockingQueue is a bounded, fixed‑capacity FIFO blocking queue implemented with a circular array. Its capacity is set at construction and cannot be changed. An optional fairness policy can be enabled to order waiting producer and consumer threads, though the default (non‑fair) mode offers higher throughput.
Class Declaration
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, Serializable {
// ...
}The class extends AbstractQueue and implements BlockingQueue, providing the core blocking‑queue semantics required for concurrent producers and consumers.
Key Fields
final Object[] items– fixed‑size array that stores the elements. int takeIndex – index of the next element to remove (head of the queue). int putIndex – index where the next element will be inserted (tail of the queue). int count – current number of elements in the queue. final ReentrantLock lock – single lock that protects both put and take operations. final Condition notEmpty – signalled when an element is added, waking waiting consumers. final Condition notFull – signalled when an element is removed, waking waiting producers. transient Itrs itrs – iterator bookkeeping (used only when an iterator is active).
Construction
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
this.lock = new ReentrantLock(fair);
this.notEmpty = lock.newCondition();
this.notFull = lock.newCondition();
}The constructor allocates the backing array and creates a ReentrantLock. If fair is true, the lock grants access to waiting threads in FIFO order.
Enqueue Operations (adding elements)
add(E e)– inserts the element, throwing IllegalStateException if the queue is full. offer(E e) – inserts the element, returning false if the queue is full. offer(E e, long timeout, TimeUnit unit) – waits up to the specified timeout for space to become available. put(E e) – waits indefinitely until space becomes available.
All insertion methods eventually call the private enqueue(E x) method, which performs the actual array update.
private void enqueue(E x) {
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0; // wrap around
count++;
notEmpty.signal(); // wake a waiting consumer
}Example implementation of offer(E e):
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false; // queue full
enqueue(e);
return true;
} finally {
lock.unlock();
}
} add(E e)simply delegates to offer and throws an exception when offer returns false:
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}Dequeue Operations (removing elements)
poll()– returns null if the queue is empty. poll(long timeout, TimeUnit unit) – waits up to the timeout for an element. remove(Object o) – removes a single instance of the specified element. take() – blocks indefinitely until an element becomes available.
The core removal logic resides in the private dequeue() method:
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null; // help GC
if (++takeIndex == items.length)
takeIndex = 0; // wrap around
count--;
if (itrs != null)
itrs.elementDequeued(); // maintain iterator state
notFull.signal(); // wake a waiting producer
return x;
}Implementation of poll() (non‑blocking):
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}Implementation of take() (blocking):
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}The difference is that poll() returns null immediately when the queue is empty, whereas take() blocks on notEmpty until a producer signals that an element has been enqueued.
Concurrency Guarantees
All public mutating operations acquire lock, guaranteeing mutual exclusion. notEmpty and notFull conditions implement the classic producer‑consumer pattern.
When fair is true, the underlying ReentrantLock services waiting threads in FIFO order, which can reduce throughput but prevents thread starvation.
Iterator Support
The queue provides a weakly consistent iterator. The internal Itrs object tracks active iterators; each call to dequeue() notifies it via elementDequeued() so that iterators can adjust their indices when elements are removed.
Performance Characteristics
All operations are O(1) because they manipulate indices and a fixed‑size array.
Memory footprint is proportional to the configured capacity plus a small overhead for the lock and condition objects.
Non‑fair mode yields higher throughput under contention, while fair mode provides predictable ordering at the cost of additional lock hand‑overhead.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
