Blocking Queues
wait() and notify() are instance methods of class Object, so any object can coordinate two threads
What Happens When the Queue Is Empty
Imagine a consumer thread that needs work items from a shared queue. Sometimes the queue has items in it, and sometimes it has nothing at all. A consumer that asks for an item from an empty queue has to be handled somehow. This article follows one consumer thread and one queue through that moment, and shows how a blocking queue keeps the consumer from either giving up or spinning in a loop.
What do you think happens?
The queue starts empty. A consumer thread calls take() on it. What happens to that consumer thread?
Reveal answer
It is suspended inside the take() call until an item is put into 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 need to poll the queue, because the waiting is part of the call itself.
Why a Plain Queue Is Not Enough
When several threads work on the same data, one common arrangement is to have some threads produce work and other threads consume it. A queue sits between them. Producers add items at one end and consumers remove items from the other. The queue must be synchronized so that two threads never corrupt it by touching it at the same moment, but synchronization alone does not solve everything.
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 spin around checking again. 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 blocking queue is a data structure that suspends a thread which tries to take from an empty queue, or to put into a full limited queue, and resumes that thread when the condition is met.
Waiting for Another Thread to Act
A blocking queue needs a way to hold a thread in a waiting state until some event happens. That event is usually caused by a different thread, for example a producer adding an item to the queue. So the problem involves at least two threads: one that waits, and one whose action wakes it up. Java provides wait() and notify() for this handoff. Both are instance methods of class Object, so any object can coordinate two threads.
The rule is simple. A thread that calls wait() on an object sleeps until another thread calls notify() on that same object. The object is the meeting point. If the two threads call wait() and notify() on different objects, they never meet and the waiting thread is not woken by that call.
Read the diagram as the life of Thread A. Thread A does not wait in every case. It waits only when a check finds that the result is not yet available. If the result already exists, the condition is false, the call to wait() is skipped, and Thread A moves straight on to using the result. When wait() is reached, Thread A stops running and stays in the waiting state until Thread B calls notify() on the same object. Only then does Thread A resume, continuing with the line after the wait. Thread B is the one running at step 2, and its notify() is what ends Thread A's pause.
When a Notification Arrives Too Early
The handoff depends on timing. A notify() call only reaches a thread that is already waiting on the same object. The source states that calling notify() when no thread is waiting is not an error and has no effect. Nothing is stored for later. The signal is simply gone.
What do you think happens?
Thread B calls notify() on an object. Thread A calls wait() on that same object only afterward. What happens to Thread A?
Reveal answer
Thread A stays waiting until another thread calls notify() on that object.
Calling notify() when no thread is waiting has no effect, so the earlier signal does not wake Thread A later. A handoff fails when the notify happens before the wait starts.
How take() Suspends and Resumes a Thread
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 diagram shows the two possible paths. 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. The thread does not need to poll the queue to find out when work has arrived. The waiting is part of the take() call itself.
Blocking applies in both directions. On the consumer side, take() on an empty queue blocks the caller until an item appears. On the producer side, 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.
Two Blocking 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 blocking operations behave the same way on either class, while the capacity rules differ.
| Question | LinkedBlockingQueue | ArrayBlockingQueue |
|---|---|---|
| What is the capacity? | Unlimited | Fixed when the queue is constructed |
| What happens to a producer when the queue is full? | Never waits, because the queue never fills | put(item) waits for room; add(item) throws IllegalStateException |
| What happens to a consumer when the queue is empty? | take() waits | take() waits |
| What keeps the queue from growing without limit? | A separate safeguard must be provided | The fixed capacity itself |
The same questions asked of each blocking queue class, based on the rules in the source.
Read the comparison from top to bottom and the pattern is clear. A bounded queue gives producers a signal of pressure, either by pausing them through put() or by raising an exception through add(). An unbounded queue removes that signal, which is why it demands a separate safeguard against unchecked growth. In both cases, a consumer that calls take() on an empty queue waits.
Choosing Between put, add, take, and clear
The source names four operations that are enough for most purposes. take() and put(item) are the blocking pair: one waits for items, 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. clear() is not part of the blocking pair described in the source, so it is not the tool for producer or consumer handoff. The source does not detail its behavior further, so the choice in a handoff rests on take, put, and add.
- The capacity 25 is fixed when the queue is constructed.
put(item)is the blocking producer operation for a limited queue.take()is the blocking consumer operation and waits without polling.- Both calls can throw
InterruptedException, which must be handled in a real program.
A producer fills a 25-item queue and then waits
An ArrayBlockingQueue has capacity 25 and starts empty. A producer calls put() for items 1 through 25, then calls put() for item 26. A consumer then calls take() once. What does each thread do, and what does the queue hold at the end?
- 1Fill the queue
Each
put()succeeds while there is room. After items 1 through 25 are added, the queue is full. - 2Producer pauses
The producer's
put()for item 26 blocks, because the limited queue is full and the producer must wait for a space to open. - 3Consumer removes one item
take()removes item 1 from the front of the queue. A space opens. - 4Producer resumes
The waiting
put()is satisfied, and the producer places item 26 into the queue, then continues from the line after theput(). - 5Check the final state
The queue now holds items 2 through 26, which is 25 items in total, so it is full again.
The producer was paused by a full queue and resumed when one take() freed a slot. Had the producer called add(item) for item 26 instead, the call would not have waited and would have thrown IllegalStateException.
Choosing between the classes is a decision about where back-pressure comes 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. A practical rule follows: if you want a natural limit on how far producers can run ahead of consumers, choose a bounded queue and use put(item). If producers must never pause and you have a separate way to bound the queue's size, choose an unbounded queue. In either case, consumers should use take(), which waits for work without polling.
Mistakes With Blocking Queues
Common mistake Polling an empty queue in a loop instead of calling take()
Why it failsRepeated checking wastes processor time.
take()already suspends the consumer until an item arrives.Do this insteadCall take() and let the call wait for work.
Common mistake Using add(item) on a bounded queue that is full and assuming it will wait
Why it failsadd(item)does not block. It throwsIllegalStateExceptionwhen a limited queue is full.Do this insteadUse put(item) when the producer should wait for room.
Common mistake Assuming notify() will wake a thread that starts waiting later
Why it failsnotify()with no waiting thread has no effect, so the signal is not stored for a later wait.Do this insteadMake sure the waiting thread checks the condition before waiting, and that the notifying call happens on the same object the other thread waits on.
Common mistake Choosing LinkedBlockingQueue and assuming the queue will stop producers from running ahead
Why it failsAn unbounded queue never makes producers wait, so it can keep growing if consumers are slower.
Do this insteadPair the unbounded queue with another way to limit its size, or choose ArrayBlockingQueue.
Practice Predicting Blocking Behavior
An ArrayBlockingQueue with capacity 3 already holds three items. A producer thread calls put(item) and a second producer calls add(item). Predict what happens to each producer. Then say what changes if one consumer thread calls take() once.
Hints
- Ask first whether the queue is full, because only a full bounded queue causes the blocking or exception behavior.
put(item)blocks the caller.add(item)does not block and throwsIllegalStateExceptionon a full limited queue.- A single
take()frees one slot, which can release a producer waiting input(item).
Summary of Blocking Queue Behavior
- 1A blocking queue suspends a consumer that calls
take()on an empty queue, and resumes it when an item is put into the queue. - 2
wait()andnotify()are instance methods of class Object. A thread that callswait()on an object sleeps until another thread callsnotify()on that same object, and a notify with no waiting thread has no effect. - 3
put(item)andtake()are the blocking pair.add(item)does not block and throwsIllegalStateExceptionwhen a limited queue is full. - 4
LinkedBlockingQueuehas unlimited capacity and never makes producers wait, so it needs a separate safeguard against unbounded growth.ArrayBlockingQueuehas a fixed capacity that provides back-pressure to producers.
At a glance
- A blocking queue suspends a consumer that calls
take()on an empty queue, and resumes it when an item arrives. wait()andnotify()coordinate two threads through a shared object, and a notify with no waiting thread has no effect.put(item)andtake()block, whileadd(item)throwsIllegalStateExceptionwhen a limited queue is full.LinkedBlockingQueuenever makes producers wait and needs a separate safeguard, whileArrayBlockingQueue's fixed capacity provides back-pressure.