Java Programs›Multithreading›Implement a Bounded Blocking Queue

Implement a Bounded Blocking Queue in Java

advanced·  Multithreading  ·  Concurrency

Problem

A bounded blocking queue holds at most a fixed number of items; producers block when it's full and consumers block when it's empty, which keeps fast producers from overwhelming slow consumers.

Implement a thread-safe queue of capacity 2 with blocking put and take, and use it to pass four jobs from a producer thread to a consumer thread.

Input
4 jobs, capacity 2
Output
jobs processed in order, then "Queue drained"

Java Program

Java
import java.util.ArrayDeque; import java.util.Deque; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class BoundedBlockingQueue { static class BoundedQueue<T> { private final Deque<T> items = new ArrayDeque<>(); private final int capacity; private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); // producers wait on this private final Condition notEmpty = lock.newCondition(); // consumers wait on this BoundedQueue(int capacity) { this.capacity = capacity; } void put(T item) throws InterruptedException { lock.lock(); try { while (items.size() == capacity) notFull.await(); // full: wait for space items.addLast(item); notEmpty.signal(); // wake one waiting consumer } finally { lock.unlock(); } } T take() throws InterruptedException { lock.lock(); try { while (items.isEmpty()) notEmpty.await(); // empty: wait for an item T item = items.removeFirst(); notFull.signal(); // wake one waiting producer return item; } finally { lock.unlock(); } } } public static void main(String[] args) throws InterruptedException { BoundedQueue<String> queue = new BoundedQueue<>(2); Thread producer = new Thread(() -> { try { for (String job : new String[]{"resize-image", "send-email", "build-report", "sync-cache"}) { queue.put(job); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); Thread consumer = new Thread(() -> { try { for (int i = 0; i < 4; i++) System.out.println("Processed " + queue.take()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); producer.start(); consumer.start(); producer.join(); consumer.join(); System.out.println("Queue drained"); } }

Output

Processed resize-image Processed send-email Processed build-report Processed sync-cache Queue drained

Core Logic

One lock guards the queue, and two conditions let threads wait for exactly the event they need: space to put, or an item to take.

How It Works
  1. 1put locks, waits on notFull while the queue is at capacity, adds the item, and signals notEmpty.
  2. 2take locks, waits on notEmpty while the queue is empty, removes the oldest item, and signals notFull.
  3. 3await() releases the lock while waiting and re-acquires it before returning.
  4. 4Unlocking in finally guarantees the lock is released even if an exception is thrown.
With room for only two jobs, if the consumer hasn't taken anything yet, the producer's third put waits until resize-image is removed — the output order is the same either way.
💡

Key Point: Two separate conditions are more efficient than one wait/notifyAll monitor: a put wakes only consumers and a take wakes only producers.

Key Concepts

ReentrantLockConditionawait()signal()

Approach 2: Using Semaphores

Java
import java.util.ArrayDeque; import java.util.Deque; import java.util.concurrent.Semaphore; public class BoundedQueueSemaphore { static class BoundedQueue<T> { private final Deque<T> items = new ArrayDeque<>(); private final Semaphore freeSlots; private final Semaphore filledSlots = new Semaphore(0); BoundedQueue(int capacity) { freeSlots = new Semaphore(capacity); // one permit per empty slot } void put(T item) throws InterruptedException { freeSlots.acquire(); // blocks while there are no empty slots synchronized (items) { items.addLast(item); } filledSlots.release(); // one more item available } T take() throws InterruptedException { filledSlots.acquire(); // blocks while there is nothing to take T item; synchronized (items) { item = items.removeFirst(); } freeSlots.release(); return item; } } public static void main(String[] args) throws InterruptedException { BoundedQueue<Integer> queue = new BoundedQueue<>(2); Thread producer = new Thread(() -> { try { for (int i = 1; i <= 4; i++) queue.put(i * 10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); producer.start(); for (int i = 0; i < 4; i++) System.out.println("Took " + queue.take()); producer.join(); } }

Output

Took 10 Took 20 Took 30 Took 40

Core Logic

Two counting semaphores track free and filled slots, so the waiting logic disappears into acquire calls.

How It Works
  1. 1freeSlots starts with one permit per empty slot; filledSlots starts at zero.
  2. 2put acquires a free slot, adds the item, and releases a filled slot.
  3. 3take does the reverse.
  4. 4A short synchronized block protects the underlying ArrayDeque itself.
If the main thread hasn't taken anything yet, the producer's third put waits in freeSlots.acquire() until 10 is taken.
💡

Key Point: Semaphores count capacity but don't make the deque thread-safe — the synchronized block around it is still required.

Key Concepts

Semaphoreacquire()release()permits

Interview Questions

Why use signal() instead of signalAll() here?

Each condition has only one kind of waiter, so waking one thread is enough. signalAll is safer when different kinds of threads wait on the same condition.

Why is await() called inside a while loop?

A thread can wake up without its condition being true — a spurious wakeup, or another thread took the item first — so it must re-check before continuing.

Learn the Concept

Related Programs