Concepts / Thread Synchronization Basics

Thread Synchronization Basics

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

  • Programming

A Queue That Makes Consumers Wait

Imagine you want to build your own blocking queue. In a blocking queue, a consumer that asks for an item does not fail when the queue is empty. Instead, it simply waits until an item arrives. To build one, you need a way to hold a thread in a waiting state until some event happens. That event is usually caused by a different thread, such as 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.

This article explains the basic mechanism Java provides for that handoff: wait() and notify(). Both are instance methods of class Object, which means any object can be used to coordinate two threads. The rest of the article traces how that handoff works step by step, and then looks at the case where things go wrong.

What do you think happens?

Suppose one thread calls notify() on an object, and only afterwards does a second thread call wait() on that same object. Does the earlier notify() wake the second thread?

Reveal answer
Answer:

No. Calling notify() when no thread is waiting is not an error and has no effect, so the later wait() still blocks.

A notify() that arrives while nobody is waiting does not leave a stored wake-up for a later wait(). The waiting thread will sleep until some other notify() happens on that object. Keep this in mind, because it is the source of the most common handoff bug covered later in the article.

Why One Thread Has to Pause

The blocking queue is the most visible example, but it is not the only one. Any time one thread produces a result that another thread needs, the order of their work is constrained. The consuming thread cannot finish its computation until the result exists. It could repeatedly check whether the result is ready, but the source describes a cleaner approach: let the consuming thread stop and wait, and then wake it up when the result is ready.

Key point

Wait and notify are the mechanism Java provides for this kind of handoff. The waiting thread is not busy-checking. It pauses, and another thread's action resumes it.

Thread A in this pattern does not always wait. It waits only when its check finds that the result is not yet available. If the result already exists, the if condition is false, the call to wait() is skipped, and Thread A moves straight on to using the result. The pause is therefore conditional: it happens only when there is something to wait for.

Tracing a Wait and Notify Handoff

The clearest way to understand the handoff is to follow the calls in time order. Two threads are involved. Thread A is the consumer that needs a result. Thread B is the thread that produces it. Both work on the same shared object, and wait() and notify() are called on that object.

  1. 1
    Thread A checks whether the result is available. It is not, so the if condition is true.
  2. 2
    Thread A calls wait() on the shared object. It stops running and enters the waiting state.
  3. 3
    Thread B produces the result, then calls notify() on the same shared object.
  4. 4
    Thread A is woken. It resumes running at the line directly after its wait() call and uses the result.
Thread AShared objectThread B1. wait() on object2. produces result, calls notify()3. Thread A resumes
In what order do the waiting thread and the notifying thread call wait() and notify(), and when does the waiting thread resume? Thread A calls wait() on the shared object and stays paused until Thread B calls notify() on that same object; only then does Thread A continue past the wait.

The diagram reads as the life of one thread. The key detail is that the notification must target the same object that the waiting thread called wait() on. Thread A stays in the waiting state until notify() is called on that object, and it resumes only then.

How a Thread Moves Between States

A thread that calls wait() on an object sleeps until another thread calls notify() on that same object. So from the waiting thread's point of view, there are only two situations to track: it is running, or it is waiting. The transition from running to waiting is caused by reaching wait(). The transition back to running is caused by a notify() on the same object.

RunningWaitingRunning againwait() reached, result not readynotify() on same object
What states does a thread move through when it calls wait(), and what moves it back to running after notify()? It moves from running to waiting when wait() is reached, and back to running when notify() is called on the same object.

Notice that the thread does not move from waiting to running on its own. Something has to call notify() on the object it is waiting on. This is why the producing thread's action is the one that wakes the consumer.

When notify() Arrives Too Early

The handoff only works when the notify() happens while the waiting thread is actually waiting. Calling notify() when no thread is waiting is not an error and has no effect. The signal does not wait around for a future wait() call. This is the failure mode that causes the handoff to go wrong.

Thread BShared objectThread A1. notify(), no waiter, no effect2. later calls wait()3. stays blocked, earlier notify was lost
If notify() runs before any thread has called wait(), what happens to the signal, and why does the later wait() block anyway? The notify() has no effect because no thread is waiting, so when Thread A later calls wait() there is no wake-up to receive, and Thread A stays paused.
Worked example

A notify that arrives before the consumer waits

Thread B produces a result and calls notify() on a shared object. Only afterwards does Thread A check the result, find it missing, and call wait() on the same object. Does Thread A wake up?

  1. 1
    Identify the notify

    Thread B calls notify() on the shared object while no thread is waiting on it.

  2. 2
    Apply the rule

    Calling notify() when no thread is waiting is not an error and has no effect. The signal is not kept for later.

  3. 3
    Reach the wait

    Thread A later checks, finds the result not available, and calls wait() on the same object.

  4. 4
    Check for a wake-up

    No notify() is pending for Thread A, because the earlier one had no effect. Thread A enters the waiting state and sleeps.

  5. 5
    Locate the failure

    The handoff went wrong at the moment Thread B called notify(). The result exists, but the consumer never receives the wake-up and stays asleep.

Result

Thread A stays blocked. The earlier notify() was lost, so the later wait() blocks anyway.

Mistakes That Break a Handoff

  • Common mistake

    Assuming notify() is remembered for a later wait()

    What it looks like

    Thread B calls notify() first, and Thread A calls wait() afterwards and expects to continue immediately.

    Why it fails

    Calling notify() when no thread is waiting has no effect, so there is no stored wake-up for the later wait().

    Do this instead

    Make sure the waiting thread reaches wait() before the notifying thread sends its notify(), or design the check so the consumer does not wait when the result already exists.

  • Common mistake

    Expecting an error when notify() has no waiter

    What it looks like

    A developer looks for an exception after calling notify() and sees nothing, then assumes the notification was delivered.

    Why it fails

    The source states that calling notify() with no waiting thread is not an error. The silence is expected behavior, not confirmation of a successful handoff.

    Do this instead

    Do not use the absence of an error as proof that the waiting thread was woken. Reason about the thread states instead.

  • Common mistake

    Notifying a different object than the one being waited on

    Why it fails

    wait() and notify() are coordinated through the same object. A thread waiting on one object sleeps until a notify() on that same object.

    Do this instead

    Use one shared object for both the wait() call and the notify() call.

  • Common mistake

    Waiting unconditionally, even when the result is already available

Choosing Higher-Level Tools

Wait and notify are the building blocks behind patterns such as blocking queues. The source notes that these methods can be used internally to implement blocking queues, and that Java has other ways of solving the wait and notify problem. The reader does not need to know whether Java's standard blocking queue classes use these methods internally.

Best practice

Knowing the wait and notify pattern is still useful even when you choose a higher-level tool. It gives you a way to reason about how any blocking structure keeps threads in step. The source advises preferring higher-level tools when they fit the problem.

Practice: Trace a Handoff

MEDIUM

A consumer thread, Thread A, finds a shared result missing and calls wait() on a shared object. Later, a producer, Thread B, makes the result ready and calls notify() on the same object. Write out the order of the calls, and state at which point Thread A resumes running. Then change the scenario so that Thread B calls notify() before Thread A calls wait(). Describe what happens to Thread A and why.

Hints
  • Start by listing which thread calls wait() and which calls notify(), and in what order.
  • Ask whether Thread A is in the waiting state at the moment notify() is called.
  • Recall whether a notify() with no waiting thread is kept for a later wait().

Key Takeaways

  • wait() and notify() are instance methods of class Object, so any object can coordinate two threads: a thread that calls wait() on an object sleeps until another thread calls notify() on that same object.
  • Waiting is useful whenever one thread needs a result or event from another thread, because the waiting thread can pause instead of repeatedly checking.
  • The wait is conditional: a thread calls wait() only when its check finds the result is not yet available, and otherwise continues without pausing.
  • Calling notify() when no thread is waiting is not an error and has no effect, so a notify() that arrives before wait() is lost and the later wait() still blocks.
  • Wait and notify underlie patterns such as blocking queues, but higher-level tools are often preferable when they fit the problem.