Back to Thread Basics & Overview
Q4Question 4 of 7

What is the difference between BLOCKED and WAITING?

Key Concepts
  • BLOCKED is an involuntary wait: the thread tried to enter a synchronized block but the lock is held by another thread.
  • WAITING is a voluntary wait: the thread called wait(), join(), or park() and gave up the CPU deliberately.
  • A BLOCKED thread resumes automatically when the lock is released; a WAITING thread needs an explicit signal.
  • wait() releases the monitor lock so the signaling thread can acquire it; a BLOCKED thread never held the lock.
  • Every Java object monitor has an entry set (BLOCKED threads) and a wait set (WAITING threads).
  • In thread dumps, many BLOCKED threads on one lock indicate contention; many WAITING threads are usually normal.

Definition

BLOCKED means the thread is forced to wait for a lock held by another thread, while WAITING means the thread voluntarily paused and needs an explicit signal to resume.
  • BLOCKED – the thread attempted to enter a synchronized block or method, but another thread already holds that monitor lock. The blocked thread had no choice: it was forced to wait by lock contention. It resumes automatically when the lock becomes available.
  • WAITING – the thread voluntarily gave up the CPU by calling wait(), join(), or LockSupport.park(). It will not resume until another thread explicitly signals it with notify(), notifyAll(), unpark(), or the joined thread terminates.
  • Key distinction – BLOCKED threads compete for a lock (involuntary). WAITING threads cooperate through signaling (voluntary). A BLOCKED thread never held the lock it is waiting for. A thread calling wait() already held the lock and released it.
  • Monitor model – every Java object has a monitor with two sets: the entry set (BLOCKED threads trying to acquire the lock) and the wait set (WAITING threads that called wait()).

BLOCKED is like arriving at the bank vault door and finding it locked by another teller inside. You must stand outside until the other teller leaves. The moment they exit, you enter automatically. WAITING is like voluntarily sitting in the break room and telling the manager to call you when the VIP customer arrives. You chose to sit down. You will not get up until the manager explicitly calls you. Nobody forced you to wait: you decided to wait for a specific event.

Understanding the Concepts

What exactly is the BLOCKED state?

A thread enters the BLOCKED state when it tries to acquire a monitor lock that is currently held by another thread. The thread did not ask to wait: it was forced by contention.

JAVA
package com.bank.core;

public class BlockedStateDemo {
    private static final Object vaultLock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread holder = new Thread(() -> {
            synchronized (vaultLock) {
                try { Thread.sleep(5000); }
                catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            }
        }, "LockHolder");

        Thread waiter = new Thread(() -> {
            synchronized (vaultLock) {
                System.out.println("Waiter acquired lock");
            }
        }, "LockWaiter");

        holder.start();
        Thread.sleep(100);
        waiter.start();
        Thread.sleep(100);

        System.out.println("Waiter state: " + waiter.getState()); // BLOCKED
        holder.join();
        waiter.join();
    }
}
TEXT
Waiter state: BLOCKED
Waiter acquired lock

What exactly is the WAITING state?

A thread enters the WAITING state when it voluntarily calls wait(), join(), or LockSupport.park(). The thread chose to pause. When a thread calls wait() inside a synchronized block, it releases the monitor lock and moves to the wait set.

JAVA
package com.bank.core;

public class WaitingStateDemo {
    private static final Object vaultLock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread waiting = new Thread(() -> {
            synchronized (vaultLock) {
                try { vaultLock.wait(); } // releases lock, enters WAITING
                catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                System.out.println("Woken up!");
            }
        }, "Waiter");

        waiting.start();
        Thread.sleep(100);
        System.out.println("Waiter state: " + waiting.getState()); // WAITING

        // Wake it up
        synchronized (vaultLock) { vaultLock.notify(); }
        waiting.join();
    }
}
TEXT
Waiter state: WAITING
Woken up!

How do the two states compare?

| Feature | BLOCKED | WAITING |
|---|---|---|
| Cause | Lock contention (involuntary) | Deliberate call: wait(), join(), park() |
| Lock held? | Thread never held the lock | Thread held the lock, then released it via wait() |
| Resume trigger | Lock becomes available (automatic) | Explicit signal: notify(), unpark(), join completes |
| Monitor set | Entry set | Wait set |
| Thread's choice | No choice: forced by contention | Voluntary: thread decided to wait |

Why does wait() release the lock?

If wait() did not release the lock, the thread that needs to call notify() could never enter the synchronized block to signal. This would cause an immediate deadlock. The design of wait() releasing the lock is essential for the wait/notify communication pattern to work.

Contrast this with Thread.sleep(), which does NOT release the lock. If you call sleep() inside a synchronized block, other threads remain BLOCKED for the entire sleep duration.

JAVA
package com.bank.core;

public class WaitReleasesLockDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        // wait() releases the lock
        Thread consumer = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Consumer: releasing lock via wait()");
                    lock.wait();
                    System.out.println("Consumer: resumed after notify()");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        consumer.start();
        Thread.sleep(200);

        // Producer can acquire the lock because wait() released it
        synchronized (lock) {
            System.out.println("Producer: acquired lock and notifying");
            lock.notify();
        }
        consumer.join();
    }
}
TEXT
Consumer: releasing lock via wait()
Producer: acquired lock and notifying
Consumer: resumed after notify()

What happens when notify() is called?

When notify() is called, one thread moves from the wait set to the entry set. It does not immediately run. It must first compete with any other BLOCKED threads to acquire the lock. Only after it reacquires the lock does it resume execution after the wait() call.

The transition is: WAITING (in wait set) then moves to the entry set, then BLOCKED (competing for lock), then RUNNABLE (lock acquired).

What are common mistakes?

A common mistake is calling Thread.sleep() inside a synchronized block when the intent is to wait for a condition. Sleep holds the lock, preventing any other thread from entering the synchronized block. Use wait() instead, which releases the lock.

JAVA
package com.bank.core;

public class SleepVsWaitMistake {
    private static final Object lock = new Object();
    private static boolean ready = false;

    // WRONG: sleep holds the lock
    public static void wrongApproach() throws InterruptedException {
        synchronized (lock) {
            while (!ready) {
                Thread.sleep(100); // Lock held for 100ms! Others blocked!
            }
        }
    }

    // CORRECT: wait releases the lock
    public static void correctApproach() throws InterruptedException {
        synchronized (lock) {
            while (!ready) {
                lock.wait(100); // Lock released! Others can proceed.
            }
        }
    }
}

How do you diagnose BLOCKED threads in production?

A thread dump taken with jstack shows each thread's state. If you see many threads BLOCKED on the same monitor, you have a lock contention bottleneck.

TEXT
"TradeWorker-42" #42 prio=5 os_prio=0 tid=0x00007f... nid=0x1a4f
    waiting for monitor entry [0x00007f...]
    java.lang.Thread.State: BLOCKED (on object monitor)
        - waiting to lock <0x00000007...> (a com.bank.model.Account)
        - locked by "TradeWorker-1" #1

Fixes for lock contention: reduce the scope of the synchronized block, use ReadWriteLock so multiple readers can proceed concurrently, or use lock-free structures like ConcurrentHashMap.

What should a senior engineer know?

  • Monitor internals: the HotSpot JVM implements monitors using a combination of biased locking (zero overhead for single-thread access), lightweight locking (CAS for low contention), and heavyweight locking (OS mutex for high contention). The lock escalates as contention increases.
  • BLOCKED vs WAITING in profilers: profiling tools like VisualVM and async-profiler categorize BLOCKED time as contention overhead. Reducing BLOCKED time directly improves throughput.
  • LockSupport.park(): this low-level method puts a thread in WAITING state without requiring a synchronized block. It is the foundation of ReentrantLock, CountDownLatch, and other java.util.concurrent utilities.

Visual Overview

DIAGRAM
  Java Object Monitor
  ┌─────────────────────────────────────────────┐
  │                                             │
  │   Entry Set (BLOCKED threads)               │
  │   ┌──────┐ ┌──────┐ ┌──────┐               │
  │   │ T2   │ │ T4   │ │ T5   │               │
  │   └──┬───┘ └──┬───┘ └──┬───┘               │
  │      │        │        │                    │
  │      ▼        ▼        ▼                    │
  │   ┌──────────────────────────┐              │
  │   │  Monitor Lock             │              │
  │   │  Currently held by: T1    │              │
  │   └──────────────────────────┘              │
  │      │                                      │
  │      │  T1 calls wait()                     │
  │      ▼                                      │
  │   Wait Set (WAITING threads)                │
  │   ┌──────┐ ┌──────┐                        │
  │   │ T1   │ │ T3   │  (called wait())        │
  │   └──────┘ └──────┘                        │
  │      ▲                                      │
  │      │  notify() moves T1 from              │
  │      │  wait set to entry set               │
  │                                             │
  └─────────────────────────────────────────────┘

Key Takeaway

BLOCKED and WAITING are both non-running states, but they differ in cause and resolution. BLOCKED is involuntary: the thread tried to enter a synchronized block and was forced to wait because another thread holds the lock. It resumes automatically when the lock is released. WAITING is voluntary: the thread called wait(), join(), or park() and chose to pause until another thread sends a signal.

The critical detail is that wait() releases the monitor lock, which allows the signaling thread to enter the synchronized block and call notify(). Sleep does not release the lock, which is why using sleep() inside a synchronized block to wait for a condition is a common bug that blocks all other threads.

In a banking thread dump, 30 threads BLOCKED on the same Account lock means lock contention: fix it by reducing the synchronized scope or switching to ReadWriteLock. Threads in WAITING state are normal: they are idle pool workers waiting for tasks.

Remember one sentence: BLOCKED is forced by lock contention and resolves automatically, while WAITING is chosen by the thread and needs an explicit signal.

---

Share
Use ← → arrow keys to navigate