Java > Java Collections Framework > Queue and Deque > BlockingQueue and ConcurrentQueue
BlockingQueue Example with Producer-Consumer
This example demonstrates how to use a BlockingQueue
to implement a producer-consumer pattern. Producers add data to the queue, and consumers retrieve and process data from the queue. The BlockingQueue
ensures thread safety and handles waiting/signaling when the queue is empty or full.
Code Implementation
This code demonstrates a basic Producer-Consumer pattern using BlockingQueue
. The Producer
generates random numbers and adds them to the queue using queue.put()
. If the queue is full, the producer blocks until space becomes available. The Consumer
retrieves numbers from the queue using queue.take()
. If the queue is empty, the consumer blocks until an element is available. LinkedBlockingQueue
is used, which is a thread-safe, optionally bounded FIFO blocking queue backed by linked nodes.
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.Random;
public class BlockingQueueExample {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10); // Capacity of 10
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
new Thread(producer).start();
new Thread(consumer).start();
Thread.sleep(5000); // Let the threads run for 5 seconds
System.out.println("Stopping producer and consumer...");
producer.stop();
consumer.stop();
}
static class Producer implements Runnable {
private BlockingQueue<Integer> queue;
private Random random = new Random();
private volatile boolean running = true;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (running) {
int number = random.nextInt(100);
queue.put(number); // Blocking put
System.out.println("Produced: " + number + ", Queue size: " + queue.size());
Thread.sleep(random.nextInt(500)); // Simulate production time
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void stop() {
running = false;
}
}
static class Consumer implements Runnable {
private BlockingQueue<Integer> queue;
private volatile boolean running = true;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (running) {
int number = queue.take(); // Blocking take
System.out.println("Consumed: " + number + ", Queue size: " + queue.size());
Thread.sleep(200); // Simulate consumption time
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void stop() {
running = false;
}
}
}
Concepts Behind the Snippet
BlockingQueue
implementations are thread-safe, eliminating the need for explicit synchronization mechanisms (e.g., locks).put()
and take()
methods block when the queue is full or empty, respectively.
Real-Life Use Case
BlockingQueue
is widely used in scenarios involving asynchronous task processing, message queues, and data streaming. For example, it can be used in a web server to handle incoming requests by placing them in a queue processed by worker threads. Another example is a log aggregation system, where log entries are produced by different parts of the application and consumed by a central logging service.
Best Practices
BlockingQueue
implementation that best suits your needs. LinkedBlockingQueue
, ArrayBlockingQueue
, PriorityBlockingQueue
, and DelayQueue
offer different characteristics.InterruptedException
when using blocking methods. Interrupting a thread waiting on a BlockingQueue
can be used to signal it to stop or perform cleanup.
Interview Tip
Be prepared to explain the producer-consumer pattern and how BlockingQueue
simplifies its implementation. Discuss the advantages of using BlockingQueue
over manual synchronization mechanisms. Also, be familiar with different BlockingQueue
implementations and their trade-offs.
When to Use Them
Use BlockingQueue
when you need to coordinate data exchange between multiple threads in a thread-safe manner and you are okay with threads blocking when the queue is full or empty. It's ideal for situations where producers and consumers operate at different speeds.
Memory Footprint
The memory footprint depends on the BlockingQueue
implementation and its capacity. LinkedBlockingQueue
's memory usage grows dynamically as elements are added, while ArrayBlockingQueue
allocates a fixed-size array upfront. Consider the expected queue size and the size of the objects stored in the queue when estimating memory consumption.
Alternatives
Collections.synchronizedList
or similar synchronized wrappers. These provide thread-safety but can lead to performance bottlenecks due to contention.Lock
s and Condition
s to implement your own queue. Provides fine-grained control but requires careful implementation to avoid errors.
Pros
Cons
InterruptedException
.
FAQ
-
What is the difference between
put()
andadd()
methods inBlockingQueue
?
Theput()
method blocks if the queue is full until space becomes available. Theadd()
method throws anIllegalStateException
if the queue is full. -
What is the difference between
take()
andpoll()
methods inBlockingQueue
?
Thetake()
method blocks if the queue is empty until an element becomes available. Thepoll()
method returnsnull
if the queue is empty. -
When should I use
ArrayBlockingQueue
vs.LinkedBlockingQueue
?
UseArrayBlockingQueue
when you have a fixed queue capacity and want to minimize memory overhead. UseLinkedBlockingQueue
when you need a dynamically sized queue, but be mindful of potential memory consumption if the queue grows unboundedly.