Java ArrayBlockingQueue: Bounded Blocking Queue Examples

Java ArrayBlockingQueue examples covering put, offer, take, fairness, producer-consumer threads and batch draining with drainTo(), vs LinkedBlockingQueue.

A producer thread calls put() on an ArrayBlockingQueue with capacity 4 whose array slots are all full, so the producer waits. A consumer thread calls take() at the head, which frees a slot and lets the producer continue.

The Java ArrayBlockingQueue class is a thread-safe FIFO queue with a fixed capacity, backed by an array, whose put() waits while the queue is full and whose take() waits while it is empty. The capacity is set in the constructor and never changes.

We use an ArrayBlockingQueue as the buffer between producer and consumer threads when producers must slow down once the consumers fall behind, for example an upload service that accepts images faster than it can resize them. The fixed size keeps memory use predictable.

The following example fills a queue with capacity 2 and shows how each insert and remove method reacts, with the result of each line as a comment.

ArrayBlockingQueue<String> uploads = new ArrayBlockingQueue<>(2);
boolean first = uploads.offer("cat.png");       // true
uploads.put("dog.png");                         // fits, the queue is now full
boolean third = uploads.offer("fox.png");       // false (full, returns at once)
boolean waited = uploads.offer("fox.png", 50, TimeUnit.MILLISECONDS);   // false (still full after 50 ms)
int free = uploads.remainingCapacity();         // 0
String oldest = uploads.take();                 // "cat.png" (FIFO)
boolean added = uploads.add("fox.png");         // true, there is room again
boolean overflow = uploads.add("owl.png");      // IllegalStateException: Queue full

Notice that a full queue gives us four choices, namely return false, wait with a timeout, wait without a limit, or throw an exception. We go through the features, the four method forms, a producer-consumer example and the constructors, and also cover drainTo() for batch processing, a comparison with LinkedBlockingQueue and common questions.

1. ArrayBlockingQueue Features

The class is one of the blocking queues of the Java Collections Framework. The class keeps the elements in one array that is used as a ring, so adding and removing never shift elements. New elements go in at the tail, and retrieval takes them from the head, which is the element that has been in the queue the longest.

A producer thread calls put() on an ArrayBlockingQueue with capacity 4 whose array slots are all full, so the producer waits. A consumer thread calls take() at the head, which frees a slot and lets the producer continue.
A full ArrayBlockingQueue makes put() wait until take() frees a slot, so producers can never run far ahead of consumers
  • The queue is bounded. The capacity is fixed at construction and cannot grow or shrink.
  • It orders elements FIFO (first-in-first-out).
  • It does not accept null and throws a NullPointerException.
  • It is thread-safe. One ReentrantLock guards all operations.
  • The iterator returns elements from head to tail and is weakly consistent, so it never throws ConcurrentModificationException.
  • An optional fairness policy makes waiting producers and consumers get access in FIFO order, as we see in section 4.

2. Four Ways to Add and Remove Elements

The BlockingQueue interface gives every operation four forms. The difference is only what happens when the operation cannot run at once, i.e. when the queue is full for inserts or empty for removals.

OperationThrows an exceptionReturns a special valueWaitsWaits with a timeout
Insertadd(e), IllegalStateExceptionoffer(e) returns falseput(e)offer(e, time, unit) returns false
Removeremove(), NoSuchElementExceptionpoll() returns nulltake()poll(time, unit) returns null
Examineelement(), NoSuchElementExceptionpeek() returns nullNot availableNot available

In a request thread, such as a REST controller, we prefer offer() with a timeout, because we can answer with an error after a short wait instead of holding the thread. A background worker that has nothing else to do calls put() and take().

ArrayBlockingQueue<String> resizeJobs = new ArrayBlockingQueue<>(1);
resizeJobs.put("a.jpg");
boolean accepted = resizeJobs.offer("b.jpg", 20, TimeUnit.MILLISECONDS);   // false (answer 503 to the client)
String peeked = resizeJobs.peek();              // "a.jpg"
String polled = resizeJobs.poll();              // "a.jpg"
String emptyPoll = resizeJobs.poll(20, TimeUnit.MILLISECONDS);   // null (nothing arrived in 20 ms)

3. Java ArrayBlockingQueue Example

An image upload service has a producer that receives uploads and a consumer that resizes them. The queue holds at most 2 images, so the producer waits whenever the resizer is 2 images behind. This waiting is called backpressure.

The following example starts both threads as virtual threads, puts 5 images through a queue of capacity 2 and waits for both threads with join(). The order in which the two threads run changes from run to run, but the result does not.

ArrayBlockingQueue<String> buffer = new ArrayBlockingQueue<>(2);
List<String> resized = new CopyOnWriteArrayList<>();

Thread producer = Thread.ofVirtual().start(() -> {
    try {
        for (int i = 1; i <= 5; i++) {
            buffer.put("img-" + i);             // waits while 2 images are queued
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
Thread consumer = Thread.ofVirtual().start(() -> {
    try {
        for (int i = 1; i <= 5; i++) {
            resized.add(buffer.take());         // waits while the queue is empty
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
producer.join();
consumer.join();
List<String> result = List.copyOf(resized);     // [img-1, img-2, img-3, img-4, img-5]
int leftOver = buffer.size();                   // 0

With one producer and one consumer, FIFO order is kept end to end. The catch blocks restore the interrupt flag, so an executor or a shutdown hook that interrupts the threads can stop them. The same producer-consumer problem can also be solved with wait() and notify(), which takes far more code.

4. Creating an ArrayBlockingQueue With Fairness

The class has three constructors, and each one needs the capacity.

  • ArrayBlockingQueue(int capacity) creates an empty queue with the default, non-fair access policy.
  • ArrayBlockingQueue(int capacity, boolean fair) sets the access policy. With true, threads that wait in put() or take() get access in FIFO order. With false, the order is not specified.
  • ArrayBlockingQueue(int capacity, boolean fair, Collection c) also adds the elements of the collection in iteration order, and throws an IllegalArgumentException when the collection is larger than the capacity.
ArrayBlockingQueue<Integer> fairQueue = new ArrayBlockingQueue<>(3, true, List.of(1, 2));
int size = fairQueue.size();                    // 2
int room = fairQueue.remainingCapacity();       // 1
ArrayBlockingQueue<Integer> tooSmall = new ArrayBlockingQueue<>(2, false, List.of(1, 2, 3));   // IllegalArgumentException

Fairness prevents one producer from waiting forever while others keep getting in, but it generally lowers throughput. We turn it on only when starvation shows up, for example when one tenant’s uploads are always rejected under load.

5. Draining the Queue with BlockingQueue drainTo()

The method drainTo() removes the available elements from a BlockingQueue and adds them to a collection in one call. It returns the number of elements moved and never waits, so it returns 0 for an empty queue.

Draining helps when producers add items one at a time but the consumer works faster in batches. For example, an audit service receives log lines from many request threads, and a writer thread inserts them into the database 100 rows at a time instead of one row per line. Draining can be more efficient than calling poll() in a loop, because ArrayBlockingQueue takes its lock only once for the whole batch.

5.1. Draining All Elements with drainTo(Collection)

The one-argument version moves every element that is in the queue at the moment of the call. The elements keep their FIFO order in the target list.

ArrayBlockingQueue<String> auditLines = new ArrayBlockingQueue<>(10, false, List.of("login", "view", "logout"));
List<String> rows = new ArrayList<>();
int written = auditLines.drainTo(rows);         // 3
List<String> drained = rows;                    // [login, view, logout]
boolean emptyNow = auditLines.isEmpty();        // true

5.2. Draining a Batch with drainTo(Collection, maxElements)

The two-argument version moves at most maxElements elements. A writer thread uses it to cap the size of each database batch.

ArrayBlockingQueue<Integer> events = new ArrayBlockingQueue<>(10, false, List.of(1, 2, 3, 4, 5));
List<Integer> batch = new ArrayList<>();
int firstBatch = events.drainTo(batch, 3);      // 3
List<Integer> batchOne = List.copyOf(batch);    // [1, 2, 3]
batch.clear();
int secondBatch = events.drainTo(batch, 3);     // 2
List<Integer> batchTwo = List.copyOf(batch);    // [4, 5]

In the audit service, several request threads add lines while the writer drains them. Two producer threads add three lines each in the next snippet, and after join() the writer drains a batch of 4.

ArrayBlockingQueue<String> logQueue = new ArrayBlockingQueue<>(100);
Thread web = Thread.ofVirtual().start(() -> {
    for (int i = 0; i < 3; i++) {
        logQueue.offer("web-" + i);
    }
});
Thread api = Thread.ofVirtual().start(() -> {
    for (int i = 0; i < 3; i++) {
        logQueue.offer("api-" + i);
    }
});
web.join();
api.join();
List<String> dbBatch = new ArrayList<>();
int batchSize = logQueue.drainTo(dbBatch, 4);   // 4 (which lines come first depends on thread timing)
int stillQueued = logQueue.size();              // 2

A real writer runs drainTo() in a loop. It first calls take() or a timed poll() to wait for at least one element, so it does not spin on an empty queue, and drains the rest of the batch right after.

The drainTo() method throws an IllegalArgumentException when the target is the queue itself, and a NullPointerException when the target is null. If the target collection rejects elements, for example an unmodifiable List.of(), an element can end up in neither, either or both collections when the exception is thrown.

ArrayBlockingQueue<String> selfTarget = new ArrayBlockingQueue<>(2, false, List.of("a"));
int bad = selfTarget.drainTo(selfTarget);       // IllegalArgumentException

6. ArrayBlockingQueue vs LinkedBlockingQueue

Both classes are FIFO blocking queues, and the choice comes down to memory and contention. The LinkedBlockingQueue class is optionally bounded and uses separate locks for the head and the tail.

AspectArrayBlockingQueueLinkedBlockingQueue
CapacityRequired, fixedOptional, default Integer.MAX_VALUE
StorageOne array, allocated up frontOne node object per element
LocksOne lock for put and takeOne lock for put, one for take
Fairness optionYesNo
Good fitFixed-size buffer, predictable memoryHigh throughput with many producers and consumers

A LinkedBlockingQueue created without a capacity is unbounded in practice, so it gives no backpressure. When we use it, we pass a capacity, for example new LinkedBlockingQueue<>(1_000). When only one thread uses the queue, a plain Queue implementation such as ArrayDeque is enough, because nothing needs to wait.

7. ArrayBlockingQueue FAQs

The common questions about ArrayBlockingQueue concern a full queue, resizing, the atomicity of drainTo() and thread pools.

7.1. What Happens When an ArrayBlockingQueue Is Full?

The reaction depends on the method. The method put() waits for space, offer() returns false, offer() with a timeout waits and returns false after the timeout, and add() throws an IllegalStateException with the message “Queue full”.

7.2. Can We Change the Capacity of an ArrayBlockingQueue?

No. The array is allocated in the constructor, and there is no method to resize it. To change the capacity, we create a new queue and move the elements with drainTo().

ArrayBlockingQueue<Integer> small = new ArrayBlockingQueue<>(2, false, List.of(1, 2));
ArrayBlockingQueue<Integer> bigger = new ArrayBlockingQueue<>(10);
int copied = small.drainTo(bigger);             // 2
int newRoom = bigger.remainingCapacity();       // 8

7.3. Is drainTo() Atomic?

For ArrayBlockingQueue, the method holds the queue lock while it removes the elements, so no other thread can add or take in between. The BlockingQueue interface does not promise that for every implementation, so we do not rely on it when the queue type can change.

7.4. Why Not Use a Synchronized ArrayList as a Queue?

A synchronizedList has no waiting operations, so consumers would have to poll in a loop, and removing from the front of an ArrayList shifts every element. An ArrayBlockingQueue waits without using CPU and removes in constant time.

7.5. Should I Use ArrayBlockingQueue With a Thread Pool?

Yes, a bounded queue is a good choice for a ThreadPoolExecutor, because it limits the number of waiting tasks. When the queue is full and the pool is at its maximum size, the executor rejects new tasks and calls its RejectedExecutionHandler.

8. Conclusion

An ArrayBlockingQueue is a fixed-size, thread-safe FIFO buffer. Its four insert forms let us choose between failing, returning false, waiting and waiting with a timeout, and the same choices exist for removal.

We use it when producers must slow down to the speed of the consumers, add fairness only when a thread starves, and drain it with drainTo() when the consumer works in batches. For an unbounded or very busy queue, LinkedBlockingQueue with an explicit capacity is the closest alternative.

9. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.