Producer/Consumer Pattern and Blocking Queues
A blocking queue suspends a thread that tries to take from an empty queue, or to put into a full limited queue, and resumes that thread when the condition is met.
Why Threads Need a Waiting Point
When several threads work on the same data, a common arrangement is to have some threads produce work while other threads consume it. Producers create items, consumers handle them, and all of them can work at the same time. This arrangement is called the producer/consumer pattern, and it appears whenever the rate at which work is created does not match the rate at which it is handled.
The pattern depends on a data structure that can pause a thread when there is nothing to do or no room to work. That structure is called a blocking queue. In this article you will follow the behavior of a blocking queue step by step: what a consumer does when the queue is empty, what a producer does when a limited queue is full, and how the queue's contents change as tasks are added and removed.
Why a Plain Queue Falls Short
A queue sits between the two groups. Producers add items at one end, and consumers remove items from the other end. The queue is the meeting point, so neither group needs to know the details of the other. However, when the producers and consumers are threads, the queue must be synchronized so that two threads never corrupt it by touching it at the same moment. Synchronization alone does not solve everything, because the waiting has to be handled as well.
Consider what a consumer should do when the queue has nothing in it. A simple design would have the consumer check the queue, see that it is empty, and then either give up or keep checking in a loop. Neither option is satisfying. Giving up loses the work the consumer was meant to do, and repeated checking wastes processor time. What is really wanted is for the consumer to pause until something arrives.
The same problem appears on the other side. A producer facing a full, limited-size queue should be able to pause until room opens up. A plain queue offers neither pause, so the programmer would have to write extra waiting logic around every access. The blocking queue exists so that this waiting is built into the queue operations themselves.
Predicting an Empty Queue
Before the mechanism is explained, commit to an answer. Imagine one consumer thread and one blocking queue. The queue starts with nothing in it, and the consumer calls take() on it. What should happen to the consumer's thread? Decide on your answer before reading on.
What do you think happens?
The queue is empty and the consumer calls take(). What happens to the consumer's thread?
Reveal answer
It is suspended (blocked) inside take() and resumed when an item is placed in the queue.
A blocking queue suspends a thread that tries to take from an empty queue and resumes that thread when the condition is met. The thread does not poll the queue; the waiting is part of the take() call itself, and the thread continues from that point once an item is available.
How a Blocked Thread Waits and Resumes
A thread that is suspended while it waits for something to happen is said to be blocked. Blocking is a pause in the thread's own progress. The thread does not finish its call and move on. It stays in the middle of the call until the condition it is waiting for is met, and then it continues from that point.
The state diagram below shows the two possible paths through take(). If an item is already present, the thread takes it and keeps going without ever pausing. If the queue is empty, the thread moves into the blocked state. It stays there until an item is placed in the queue, and at that moment it resumes and receives the item. Notice that the thread never has to repeatedly check the queue to find out when work has arrived.
Blocking applies in both directions. On the consumer side, a call to take() on an empty queue blocks the caller until an item appears. On the producer side, a call to put(item) on a limited-capacity queue that is already full blocks the caller until a space opens. The same queue can therefore hold back consumers when it is empty and hold back producers when it is full, without any extra logic written by the programmer around the queue.
The Handoff from Producer to Consumer
A producer/consumer arrangement has one or more producers creating tasks and one or more consumers handling them, with all of them able to work at the same time. The handoff point is the queue: producers place items into it as they produce them, and consumers take items off the opposite end.
The diagram below shows the flow. Each producer calls put(item) to hand work to the queue, and each consumer calls take() to receive work from it. Neither a producer nor a consumer needs to know about the other; each only needs to know about the queue. The queue is therefore the single place where work changes hands.
The two groups do not run at the same pace, and the pattern has to cope with that. Sometimes a consumer finds nothing ready to process, and it must wait until a producer makes something. This waiting is a normal part of the pattern, not an error. The reverse situation matters just as much: if producers keep producing faster than consumers can handle, the backlog grows without bound unless a limit is placed on how many items may wait in the queue.
Tracing a Queue of Tasks
A Producer Fills a 25-Item Queue
A bounded queue has capacity 25. A producer keeps calling put(item). What happens when the queue reaches its limit, and what does a single removal by a consumer change?
- 1Fill the queue
Each
put(item)adds one item. After 25 successful puts the queue is full, because the capacity was fixed at 25 when the queue was constructed. - 2Attempt the 26th put
The producer calls
put(item)again. The queue is full and is limited, soput(item)blocks the producer. The producer is paused inside the call rather than discarding the item or spinning in a loop. - 3A consumer removes one item
A consumer calls
take(), which removes one item from the queue. This frees one space in the full queue. - 4The producer resumes
The blocked producer is resumed and its item is placed into the queue. The queue is full again, with 25 items, and the producer continues from the point where it paused.
The capacity limit acts as a signal of pressure. The producer runs ahead of the consumers only until the queue is full, and then it waits for a consumer to remove something before adding more.
The timeline below traces a smaller sequence of operations on an empty queue. Read it from top to bottom, and note that the queue returns items in the order they were added: the first task placed in is the first task taken out.
Choosing Between the Two Queue Classes
Java provides two classes that implement blocking queues: LinkedBlockingQueue and ArrayBlockingQueue. Both are parameterized types, so you specify the type of item the queue will hold. Both are defined in the package java.util.concurrent, and both implement an interface called BlockingQueue. Because they share that interface, the take() and put(item) operations behave the same way on either class, while the capacity rules differ.
| Question | LinkedBlockingQueue | ArrayBlockingQueue |
|---|---|---|
| Capacity | Unlimited | Fixed at construction |
| put(item) when full | Never full, so producers never wait | Blocks the producer until space opens |
| add(item) when full | Never full, so no exception from capacity | Throws IllegalStateException |
| take() when empty | Blocks the consumer until an item appears | Blocks the consumer until an item appears |
| Pressure on producers | None from the queue itself | The queue itself slows producers down |
Behavior of the two blocking queue classes, as described by the source
Choosing between the classes is really a decision about where back-pressure should come from. A bounded ArrayBlockingQueue lets the queue itself slow producers down when consumers fall behind. An unbounded LinkedBlockingQueue lets producers keep going, which avoids blocking them, but then the queue can keep growing if consumers are slower than producers.
- The queue type is declared as the
BlockingQueueinterface, and the concrete class isArrayBlockingQueuewith a fixed capacity of 25. put(item)is the blocking producer operation for a limited queue.take()is the blocking consumer operation; it waits for work instead of polling.add(item)is the non-blocking counterpart ofput(item)and fails when a limited queue is full.
Choosing put, add, take, and clear
Take() and put(item) are the blocking pair: one waits for items, and the other waits for room. Add(item) is the non-blocking counterpart of put(item). The source recommends put(item) for limited-capacity queues and add(item) for unlimited ones, because add(item) cannot wait for room that will never be needed. Consumers should use take(), which waits for work without polling.
- The caller must wait for an item to arrive: use
take(). - The producer must wait for room in a limited queue: use
put(item). - The producer must not wait and can accept a failure when the queue is full: use
add(item)on a limited queue, and handle theIllegalStateExceptionthat it throws. - The queue is unbounded and the producer should never wait:
add(item)is the matching choice, provided another mechanism limits the queue's growth. - The queue must be emptied: use
clear(). The source namesclear()among the operations to choose from but does not describe its behavior in detail, so check its documentation rather than assuming how it treats waiting threads.
Why Thread Pools Need Idle Waiting
Consider a program that draws images, where each image takes real computation to produce. The source course uses an example called MultiprocessingDemo2, which creates an entirely new thread pool each time it draws an image. That looks wasteful, and the obvious improvement is to create one set of threads when the program starts and reuse them whenever an image needs computing.
A thread pool only works well if its threads can sit idle until work arrives and then carry that work out. Without a blocking queue there is no direct way to make a thread wait for a task to show up. The blocking queue fills that gap: idle pool threads call take() on the queue, block while nothing is available, and resume as soon as a producer hands them a task.
Mistakes Beginners Make
Common mistake Expecting add(item) to wait when a limited queue is full
What it looks likeA producer calls add(item) on an ArrayBlockingQueue that already holds its full capacity and assumes the thread will pause until space opens.
Why it failsadd(item)does not block. On a limited queue that is full, it throwsIllegalStateException.Do this insteadUse put(item) when the producer should pause until room opens, and use add(item) only when failure is an acceptable outcome.
Common mistake Polling an empty queue in a loop instead of calling take()
What it looks likeA consumer checks whether the queue is empty, sleeps briefly, and checks again, repeating until an item appears.
Why it failsRepeated checking wastes processor time.
take()already suspends the thread until an item arrives, so the loop is unnecessary.Do this insteadCall take() and let the blocking queue suspend the consumer until a producer places an item in the queue.
Common mistake Assuming an unbounded queue limits itself
What it looks likeA team uses a LinkedBlockingQueue and assumes producers will slow down when consumers fall behind.
Why it failsAn unbounded
LinkedBlockingQueuenever makes producers wait, so nothing in the queue itself prevents the backlog from growing without limit.Do this insteadPair the unbounded queue with a separate mechanism that limits how many items may wait, or choose a bounded queue and use put(item).
Common mistake Assuming synchronization alone solves the handoff: synchronization prevents corruption of the queue, but it does not make a consumer wait for work or a producer wait for room.
Practice Tracing a Blocking Queue
A bounded queue with capacity 2 starts empty. Trace these calls in order: put(A), put(B), add(C), take(), take(), take(). For each call, state the queue contents afterward, and say whether the calling thread would block, throw an exception, or continue. Which call, if any, does not complete immediately?
Hints
- After
put(A)andput(B)the queue is full, because its capacity is 2. add(C)on a full limited queue does not block; think about what it throws.- Remember that
take()removes from the front, so the first item taken is A. - The third
take()happens after the queue is empty, so think about whattake()does then.
Key Takeaways
- A blocking queue suspends a thread that tries to take from an empty queue, or to put into a full limited queue, and resumes that thread when the condition is met.
- In a producer/consumer arrangement, producers add items at one end of the queue and consumers remove them from the other, so the queue is the handoff point between the two groups.
LinkedBlockingQueuehas unlimited capacity and never makes producers wait, whileArrayBlockingQueuehas a capacity fixed at construction;put(item)blocks on a full limited queue, andadd(item)throwsIllegalStateExceptioninstead.take()waits for work without polling, which lets idle consumers and idle thread pool threads wait for tasks without wasting processor time.- An unbounded queue must be paired with another safeguard against unchecked growth, and a limit on pending tasks is what makes producers pause when consumers fall behind.