Java · Java Multithreading

Java: wait/notify, Deadlock and Liveness

Guarded blocks with wait and notifyAll (and why notify hangs a producer-consumer), wait versus sleep, deadlock and lock ordering, livelock and starvation.

  • 9 min read
  • 12 practice questions
  • 1 graded problem
On this page
  1. Description
  2. Waiting for a condition
  3. Why while, and why notifyAll
  4. wait versus sleep, and better tools
  5. Deadlock
  6. Livelock and starvation
  7. Common mistakes
  8. Check yourself
  9. Key takeaways
  10. Quick revise
  11. Practice

Description

Threads often need to wait for each other. A consumer waits until there's work in a queue; a producer waits until there's room; a payment waits until two accounts are both available. Getting this coordination right is what separates concurrent code that runs for months from code that freezes at 3 a.m. with every thread asleep and nothing in the logs.

There are three ways threads get stuck: waiting for something that never comes, waiting for each other in a circle (deadlock), and staying busy without getting anywhere (livelock and starvation). This skill covers all three, plus Java's built-in tool for waiting: wait and notify. Interviews love this area ("implement a bounded blocking queue", "why does this transfer code hang?"), and so do production incidents.

Here's the path, from easiest to most technical:

  1. Waiting for a condition: wait, notify and the guarded-block pattern.
  2. Why it's while and not if, and why notifyAll is usually the safe choice.
  3. wait versus sleep, and the higher-level tools to prefer.
  4. Deadlock: how two threads freeze each other, and lock ordering.
  5. Livelock and starvation.

Waiting for a condition

Say a consumer thread needs an item that a producer hasn't made yet. It could loop and check over and over, but that burns CPU. Instead, Java lets a thread wait until another thread tells it something changed. The tools are three methods every object has: wait(), notify() and notifyAll().

The rules:

  • You must hold the object's lock (be inside synchronized (obj)) to call obj.wait() or obj.notify(). Otherwise you get an IllegalMonitorStateException.
  • wait() releases the lock and puts the thread to sleep until it's notified (or interrupted). Before wait() returns, the thread takes the lock back.
  • notify() wakes one waiting thread; notifyAll() wakes all of them, and they take turns getting the lock.

Every correct use follows one pattern, called a guarded block: wait in a loop while the condition you need isn't true yet.

synchronized (lock) {
    while (!condition) {      // while, never if
        lock.wait();
    }
    // here the condition is true, and we hold the lock
}

And whoever changes the condition notifies the waiters while holding the same lock:

synchronized (lock) {
    condition = true;
    lock.notifyAll();
}

Why while, and why notifyAll

Why while? Being woken up only means "something changed", not "your condition is true". By the time the woken thread gets the lock back, another thread may have taken the item it was waiting for. A thread can even wake up with no notification at all (a spurious wakeup, which the JVM specification allows). The loop rechecks the condition every time; an if would carry on as if it were true.

Why notifyAll? Here's a one-slot buffer that passes items from producers to consumers:

synchronized void put(int x) throws InterruptedException {
    while (item != null) wait();      // wait for an empty slot
    item = x;
    notify();
}
synchronized int take() throws InterruptedException {
    while (item == null) wait();      // wait for an item
    int x = item;
    item = null;
    notify();
    return x;
}

It uses while loops and works with one producer and one consumer. With two of each, it hung in three test runs out of three, with every thread asleep in wait(). Producers and consumers wait on the same object, and notify() wakes just one waiter, chosen arbitrarily. A consumer that takes the item can wake another consumer instead of a producer. That consumer finds nothing, goes back to sleep, and now nobody is left to wake anyone. With notifyAll(), the same test finished every time.

So use notifyAll() unless every waiter waits for the same thing and any one of them can make progress.

wait versus sleep, and better tools

Thread.sleep() looks similar but behaves very differently inside a synchronized block: it keeps the lock for the whole sleep, so every other thread that needs the lock is stuck too. wait() releases it.

wait()sleep()
Needs the lock to callyesno
Releases the lock while waitingyesno
Woken bynotify/notifyAll, interrupt, timeoutthe time running out, interrupt

In everyday code you'll rarely write wait/notify yourself. java.util.concurrent has ready-made tools built on the same idea: a BlockingQueue (its put and take are exactly the producer/consumer code above, done right), CountDownLatch and Semaphore (the Concurrency module covers them). They're still worth understanding, because "implement a bounded blocking queue" is a classic interview question.

Deadlock

A bank transfer locks both accounts, so no other thread sees money in flight:

void transfer(Account from, Account to, long cents) {
    synchronized (from) {
        synchronized (to) {
            from.withdraw(cents);
            to.deposit(cents);
        }
    }
}
// thread 1: transfer(a, b, 100)        thread 2: transfer(b, a, 50)

If both transfers start at the same moment, this happens:

waits for

held by

waits for

held by

Thread 1: holds a

lock b

Thread 2: holds b

lock a

waits for

held by

waits for

held by

Thread 1: holds a

lock b

Thread 2: holds b

lock a

Each thread holds one lock and waits for the other's, forever. This is a deadlock: a cycle of threads each waiting for the next. Nothing crashes and nothing is logged; the threads just stop.

The standard fix is lock ordering: every thread takes locks in the same global order, so a cycle can't form. Here, always lock the account with the smaller id first:

Account first = from.id() < to.id() ? from : to;
Account second = (first == from) ? to : from;
synchronized (first) {
    synchronized (second) {
        from.withdraw(cents);
        to.deposit(cents);
    }
}

Two more habits help: don't call unknown code while holding a lock (a listener or callback might take locks of its own, in an order you don't control), and, with ReentrantLock, use tryLock with a timeout so a thread gives up instead of waiting forever.

When a service does freeze, a thread dump (jstack <pid>) finds the cycle for you: it ends with Found one Java-level deadlock: and names the threads and the locks each holds and wants.

Livelock and starvation

A deadlocked thread sits still. A livelocked thread keeps busy without getting anywhere. Two threads that each grab one lock, try the other with tryLock, fail, let go and retry immediately can collide in perfect step forever, like two people in a corridor who keep stepping aside the same way. A short random pause before retrying breaks the pattern.

Starvation is a thread that could run but never gets its turn. Java's built-in locks make no fairness promise, so under heavy contention one thread can keep losing the race for a lock. A thread that holds a lock for a long time starves everyone queued behind it. The fixes are shorter critical sections, a fair lock (new ReentrantLock(true), at some cost in speed), or a queue that serves waiters in order.

Common mistakes

MistakeWhat happensDo this instead
wait() or notify() outside synchronizedIllegalMonitorStateExceptioncall them while holding that object's lock
if (!condition) wait();carries on when the condition isn't truewhile (!condition) wait();
notify() with producers and consumers on one lockthe wrong thread wakes; everyone can end up asleepnotifyAll()
sleep() inside synchronized to wait for somethingeveryone else is locked outwait(), or a BlockingQueue
Nested locks taken in different ordersdeadlockone global lock order
Calling listeners while holding a lockhidden lock-order cyclescall them after releasing the lock
Retrying immediately after a failed tryLocklivelockretry after a short random pause

Check yourself

Try to answer before opening each one. Recalling beats re-reading.

1. Why must wait() sit inside a while loop rather than an if?

Show answer

Waking up only means something changed. Another thread may have changed the condition back before this one got the lock, and spurious wakeups are possible. The loop rechecks the condition each time.

2. What's the difference between wait() and sleep() inside a synchronized block?

Show answer

wait() releases the lock while it waits (and needs the lock to be called). sleep() keeps every lock the thread holds, so other threads are blocked for the whole sleep.

3. A one-slot buffer with notify() works with one producer and one consumer but hangs with two of each. Why?

Show answer

notify() wakes one arbitrary waiter. A consumer can wake another consumer instead of a producer; that consumer finds no item and waits again, and nobody is left to wake the producers. notifyAll() wakes everyone, so the right kind of thread always gets a chance.

4. transfer(a, b) and transfer(b, a) each lock from and then to. How do you make deadlock impossible?

Show answer

Take the locks in one global order, for example always the account with the smaller id first, so no cycle can form.

Key takeaways

  • wait releases the lock and needs it to be called; sleep keeps it.
  • Guarded blocks: while (!condition) wait();, and notifyAll() unless every waiter is interchangeable.
  • In real code, prefer BlockingQueue, latches and semaphores.
  • A deadlock is a cycle of waiting threads: order your locks and don't call unknown code while holding one.
  • Livelock needs a random backoff; starvation needs shorter or fairer locking.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

The guarded block

synchronized (lock) {                 // waiter
    while (!condition) lock.wait();   // while, never if
    // condition holds, lock held
}
synchronized (lock) {                 // changer
    condition = true;                 // change state first...
    lock.notifyAll();                 // ...then notify, under the same lock
}

wait versus sleep

wait()sleep()
Must hold the lockyes (else IllegalMonitorStateException)no
Releases the lockyesno
Ends onnotify/notifyAll, interrupt, timeouttimeout, interrupt

The bounded buffer with Conditions

lock.lock();
try {
    while (items.size() == capacity) notFull.await();
    items.addLast(x);
    notEmpty.signal();               // separate wait sets: signal can't wake the wrong side
} finally { lock.unlock(); }

Deadlock

  • Four conditions: mutual exclusion, hold-and-wait, no preemption, circular wait. Break the cycle.
  • Lock ordering: always take locks in one global order (e.g. smaller id first).
  • Don't call listeners or unknown code while holding a lock.
  • tryLock(timeout) turns a hang into an error.
  • jstack: "Found one Java-level deadlock". ThreadMXBean.findDeadlockedThreads().

The traps

  • if instead of while around wait().
  • notify() when producers and consumers share one lock.
  • sleep() inside synchronized.
  • Nested locks in different orders on different code paths.
  • Retrying a failed tryLock immediately (livelock): back off randomly.
  • Long critical sections and unfair locks: starvation.

Staff-level points, one line each

  • Monitor = entry set (BLOCKED) + wait set (WAITING); notify moves a waiter to the entry set, where newcomers can beat it.
  • Spurious wakeups come from OS condition variables; the spec allows them.
  • Lost wakeup: a bare wait() after the notify sleeps forever; the condition variable is the memory notify lacks.
  • Deadlocks with no lock cycle: a task waiting on a subtask in the same full pool; nested checkouts from one connection pool. findDeadlockedThreads() sees neither.
  • Databases detect deadlocks and abort a victim transaction: catch and retry.
  • At scale: one lock per aggregate, single-owner state, optimistic concurrency, timeouts, exponential backoff with jitter.
  • Priority inversion (Mars Pathfinder, 1997); Java has no priority inheritance.

Say it in an interview

  • "I'd use while, because a woken thread has to recheck: spurious wakeups, or someone else took the item."
  • "notifyAll, since producers and consumers wait on the same monitor; or two Conditions with signal."
  • "I'd order the locks by account id, so a cycle can't form."
  • "A pool thread must never block on work queued to its own pool."

Practice

This skill comes with 12 multiple-choice questions that tell you which misconception a wrong answer reveals and 1 graded problem. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Java: wait/notify, Deadlock and Liveness free

A Bounded Order Queue

MediumProCoding problemJava

A food-delivery platform in Chicago hands new orders from web threads (producers) to dispatch threads (consumers) through an in-memory queue. If dispatch falls behind, the queue must not grow without limit: producers have to wait for room.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Java: wait/notify, Deadlock and Liveness free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime