Mastering Unbounded and Bounded Queues in Java: When to Use Each
This article explains the concepts, characteristics, and ideal scenarios for unbounded and bounded queues in Java, provides step‑by‑step Maven setup and complete code examples for asynchronous task scheduling, event‑driven processing, and API rate‑limiting, and highlights practical considerations such as resource management and performance.
Unbounded Queue (Unbounded Queue)
An unbounded queue has no logical limit on the number of elements it can hold, so producers can keep adding items without blocking or throwing exceptions.
Key Characteristics
Dynamic Expansion : Grows memory as needed to accommodate more elements.
High Concurrency Friendly : Handles large volumes of requests without blocking.
Memory Consumption : Unlimited growth can lead to high memory usage or out‑of‑memory errors if not monitored.
Typical Use Cases
Task Scheduling : Producers enqueue tasks that consumers process asynchronously.
Event Handling : Event‑driven systems receive a flood of events and process them in separate consumer threads.
Sample Implementation – Asynchronous Task Scheduler
The following Maven dependency is required for logging (SLF4J):
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
</dependencies>Java code (using LinkedBlockingQueue as an unbounded queue):
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class Task {
private final String name;
public Task(String name) { this.name = name; }
public String getName() { return name; }
@Override public String toString() { return "Task{name='" + name + "'}"; }
}
class TaskProducer implements Runnable {
private final BlockingQueue<Task> taskQueue;
public TaskProducer(BlockingQueue<Task> taskQueue) { this.taskQueue = taskQueue; }
@Override public void run() {
int count = 0;
while (true) {
try {
Task task = new Task("Task-" + count++);
System.out.println("Producing " + task);
taskQueue.put(task);
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
class TaskConsumer implements Runnable {
private final BlockingQueue<Task> taskQueue;
public TaskConsumer(BlockingQueue<Task> taskQueue) { this.taskQueue = taskQueue; }
@Override public void run() {
while (true) {
try {
Task task = taskQueue.take();
System.out.println("Consuming " + task);
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
public class AsyncTaskScheduler {
public static void main(String[] args) {
BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>();
Thread producer = new Thread(new TaskProducer(taskQueue));
Thread consumer = new Thread(new TaskConsumer(taskQueue));
producer.start();
consumer.start();
try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
finally { producer.interrupt(); consumer.interrupt(); }
}
}Explanation of the Code
Task class : Represents a unit of work with a name and a readable toString() method.
TaskProducer : Implements Runnable, continuously creates new Task objects, prints a log, and puts them into the queue, pausing 100 ms between creations.
TaskConsumer : Also implements Runnable, repeatedly takes tasks from the queue, logs consumption, and simulates processing with a 200 ms sleep.
AsyncTaskScheduler : Sets up the shared LinkedBlockingQueue, starts producer and consumer threads, lets them run for 5 seconds, then interrupts both to shut down gracefully.
Event‑Driven Example Using an Unbounded Queue
The same pattern applies to event processing: events are produced, placed in a LinkedBlockingQueue, and consumed asynchronously.
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class Event {
private final String message;
public Event(String message) { this.message = message; }
public String getMessage() { return message; }
@Override public String toString() { return "Event{message='" + message + "'}"; }
}
class EventProducer implements Runnable {
private final BlockingQueue<Event> eventQueue;
public EventProducer(BlockingQueue<Event> eventQueue) { this.eventQueue = eventQueue; }
@Override public void run() {
int count = 0;
while (true) {
try {
Event ev = new Event("Event-" + count++);
System.out.println("Producing " + ev);
eventQueue.put(ev);
Thread.sleep(50);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
}
}
}
class EventConsumer implements Runnable {
private final BlockingQueue<Event> eventQueue;
public EventConsumer(BlockingQueue<Event> eventQueue) { this.eventQueue = eventQueue; }
@Override public void run() {
while (true) {
try {
Event ev = eventQueue.take();
System.out.println("Consuming " + ev);
Thread.sleep(100);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
}
}
}
public class EventDrivenSystem {
public static void main(String[] args) {
BlockingQueue<Event> eventQueue = new LinkedBlockingQueue<>();
Thread producer = new Thread(new EventProducer(eventQueue));
Thread consumer = new Thread(new EventConsumer(eventQueue));
producer.start();
consumer.start();
try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
finally { producer.interrupt(); consumer.interrupt(); }
}
}Bounded Queue (Bounded Queue)
A bounded queue imposes a fixed maximum capacity. When the limit is reached, further insertions block or fail, providing natural flow‑control.
Key Characteristics
Fixed Capacity : Defined at creation; exceeding it blocks producers.
Flow Control : Prevents system overload by throttling incoming requests.
Memory Management : Caps memory usage, avoiding unbounded growth.
Typical Use Cases
Producer‑Consumer Model : Guarantees that producers cannot outpace consumers, protecting memory.
Rate Limiting : Controls request throughput in high‑traffic APIs.
Producer‑Consumer Example Using ArrayBlockingQueue
Maven dependency (same SLF4J as above) is required.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class Task {
private final String name;
public Task(String name) { this.name = name; }
@Override public String toString() { return "Task{name='" + name + "'}"; }
}
class TaskProducer implements Runnable {
private final BlockingQueue<Task> taskQueue;
public TaskProducer(BlockingQueue<Task> taskQueue) { this.taskQueue = taskQueue; }
@Override public void run() {
int count = 0;
while (true) {
try {
Task task = new Task("Task-" + count++);
System.out.println("Producing " + task);
taskQueue.put(task); // blocks if queue is full
Thread.sleep(100);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
}
}
}
class TaskConsumer implements Runnable {
private final BlockingQueue<Task> taskQueue;
public TaskConsumer(BlockingQueue<Task> taskQueue) { this.taskQueue = taskQueue; }
@Override public void run() {
while (true) {
try {
Task task = taskQueue.take();
System.out.println("Consuming " + task);
Thread.sleep(200);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
}
}
}
public class ProducerConsumerProblem {
public static void main(String[] args) {
BlockingQueue<Task> taskQueue = new ArrayBlockingQueue<>(5); // capacity 5
Thread producer = new Thread(new TaskProducer(taskQueue));
Thread consumer = new Thread(new TaskConsumer(taskQueue));
producer.start();
consumer.start();
try { Thread.sleep(10000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
finally { producer.interrupt(); consumer.interrupt(); }
}
}Rate‑Limiting Example Using a Bounded Queue
This demonstrates how an ArrayBlockingQueue can throttle API requests.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class Request {
private final String clientId;
public Request(String clientId) { this.clientId = clientId; }
@Override public String toString() { return "Request{clientId='" + clientId + "'}"; }
}
class RequestProducer implements Runnable {
private final BlockingQueue<Request> requestQueue;
public RequestProducer(BlockingQueue<Request> requestQueue) { this.requestQueue = requestQueue; }
@Override public void run() {
int count = 0;
while (true) {
try {
Request req = new Request("Client-" + count++);
System.out.println("Producing " + req);
requestQueue.put(req); // blocks when full
Thread.sleep(50);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
}
}
}
class RequestConsumer implements Runnable {
private final BlockingQueue<Request> requestQueue;
public RequestConsumer(BlockingQueue<Request> requestQueue) { this.requestQueue = requestQueue; }
@Override public void run() {
while (true) {
try {
Request req = requestQueue.take();
System.out.println("Consuming " + req);
Thread.sleep(100);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
}
}
}
public class RateLimitingControl {
public static void main(String[] args) {
BlockingQueue<Request> requestQueue = new ArrayBlockingQueue<>(5);
Thread producer = new Thread(new RequestProducer(requestQueue));
Thread consumer = new Thread(new RequestConsumer(requestQueue));
producer.start();
consumer.start();
try { Thread.sleep(10000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
finally { producer.interrupt(); consumer.interrupt(); }
}
}Key Takeaways
Use an unbounded queue when you need to handle large, unpredictable workloads and can tolerate higher memory usage.
Choose a bounded queue when memory is limited or you must enforce strict flow‑control to prevent overload.
Both patterns rely on the producer‑consumer model, where producers add work items and consumers process them concurrently.
Proper thread management, graceful shutdown, and thoughtful exception handling are essential for robust implementations.
Java Architecture Stack
Dedicated to original, practical tech insights—from skill advancement to architecture, front‑end to back‑end, the full‑stack path, with Wei Ge guiding you.
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.
