Concepts / Blocking Queues

Blocking Queues

wait() and notify() are instance methods of class Object, so any object can coordinate two threads

  • Programming

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?

  • It returns null immediately and keeps running

  • It throws an exception and the thread stops

  • It is suspended inside the take() call until an item is put into the queue

  • It repeatedly checks the queue until an item appears

Reveal answer
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.

Definition

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.

Thread Awaiting consumerShared objectsame object for both callsThread Bproducer1. wait() on object2. notify() on same object3. Thread A resumes after wait()
In what order do the calls to wait() and notify() happen across the two threads, and which thread is running at each step?

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.

notify() calledno thread is waitingSignal has no effectnothing is storedThread A calls wait()starts waiting too lateThread A sleepsuntil another notify()signal droppedlaterwaits
If notify() fires before the other thread starts waiting, what happens to the signal, and at which point in time does the handoff fail?

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?

  • Thread A resumes immediately because the notify was already sent

  • Thread A stays waiting until another thread calls notify() on that object

  • Thread A throws an exception

Reveal answer
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.

Runningconsumer calls take()Queue empty?check the queueRunning againreceives the itemBlockedsuspended inside take()take()item present: takes it without pausingqueue emptyitem placed in queue
What states does the consumer thread move through when take() finds the queue empty, and what event moves it back to running?

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.

QuestionLinkedBlockingQueueArrayBlockingQueue
What is the capacity?UnlimitedFixed when the queue is constructed
What happens to a producer when the queue is full?Never waits, because the queue never fillsput(item) waits for room; add(item) throws IllegalStateException
What happens to a consumer when the queue is empty?take() waitstake() waits
What keeps the queue from growing without limit?A separate safeguard must be providedThe 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.

java
Notes on the code
  • 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.
Worked example

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?

  1. 1
    Fill the queue

    Each put() succeeds while there is room. After items 1 through 25 are added, the queue is full.

  2. 2
    Producer 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.

  3. 3
    Consumer removes one item

    take() removes item 1 from the front of the queue. A space opens.

  4. 4
    Producer resumes

    The waiting put() is satisfied, and the producer places item 26 into the queue, then continues from the line after the put().

  5. 5
    Check the final state

    The queue now holds items 2 through 26, which is 25 items in total, so it is full again.

Result

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.

Queue full25 items, capacity 25put(item)producer callProducer blockedwaits for a spaceIllegalStateExceptionfrom add(item)producer adds itemput() pathadd() path
When a bounded queue is full, what does the calling thread do under put() versus add(), and how does the queue's state change afterward?

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 fails

    Repeated checking wastes processor time. take() already suspends the consumer until an item arrives.

    Do this instead

    Call 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 fails

    add(item) does not block. It throws IllegalStateException when a limited queue is full.

    Do this instead

    Use put(item) when the producer should wait for room.

  • Common mistake

    Assuming notify() will wake a thread that starts waiting later

    Why it fails

    notify() with no waiting thread has no effect, so the signal is not stored for a later wait.

    Do this instead

    Make 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 fails

    An unbounded queue never makes producers wait, so it can keep growing if consumers are slower.

    Do this instead

    Pair the unbounded queue with another way to limit its size, or choose ArrayBlockingQueue.

Practice Predicting Blocking Behavior

MEDIUM

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 throws IllegalStateException on a full limited queue.
  • A single take() frees one slot, which can release a producer waiting in put(item).

Summary of Blocking Queue Behavior

  1. 1
    A blocking queue suspends a consumer that calls take() on an empty queue, and resumes it when an item is put into the queue.
  2. 2
    wait() and notify() are instance methods of class Object. A thread that calls wait() on an object sleeps until another thread calls notify() on that same object, and a notify with no waiting thread has no effect.
  3. 3
    put(item) and take() are the blocking pair. add(item) does not block and throws IllegalStateException when a limited queue is full.
  4. 4
    LinkedBlockingQueue has unlimited capacity and never makes producers wait, so it needs a separate safeguard against unbounded growth. ArrayBlockingQueue has 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() and notify() coordinate two threads through a shared object, and a notify with no waiting thread has no effect.
  • put(item) and take() block, while add(item) throws IllegalStateException when a limited queue is full.
  • LinkedBlockingQueue never makes producers wait and needs a separate safeguard, while ArrayBlockingQueue's fixed capacity provides back-pressure.