Fix java.lang.IllegalMonitorStateException: current thread is not owner When Using wait() or notify()

intermediateโ˜• Java2026-06-27| Java 8+, any OS (Windows / Linux / macOS), any JVM-based application using Object.wait(), Object.notify(), or Object.notifyAll()

Error Message

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread is not owner
#java#concurrency#multithreading

The Problem

You're building a producer-consumer pattern or a custom blocking queue โ€” something that involves threads waiting on each other. At runtime, one of them crashes with:

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread is not owner
    at java.base/java.lang.Object.wait(Native Method)
    at com.example.SharedQueue.waitForItem(SharedQueue.java:14)
    at com.example.ConsumerThread.run(ConsumerThread.java:22)

The stack trace points to a line calling wait(), notify(), or notifyAll(). That thread is dead. Depending on your design, the remaining threads either deadlock or spin forever waiting for a signal that never comes.

Root Cause

Java has one hard rule about these three methods: you can only call wait(), notify(), or notifyAll() on an object if the current thread holds that object's monitor lock. This isn't a soft guideline โ€” if the lock isn't held, the JVM throws IllegalMonitorStateException immediately, no second chances.

Here's the broken pattern that triggers it:

// BROKEN: calling wait() and notify() without a synchronized block
public class SharedQueue {
    private final List<String> queue = new ArrayList<>();

    public void waitForItem() throws InterruptedException {
        while (queue.isEmpty()) {
            queue.wait(); // <-- throws IllegalMonitorStateException
        }
    }

    public void addItem(String item) {
        queue.add(item);
        queue.notify(); // <-- throws IllegalMonitorStateException
    }
}

The thread calling queue.wait() never acquired the lock on queue. No lock, no wait.

Debug: Find the Exact Mismatch

The stack trace gives you the exact line. Then ask two questions:

  • Is the wait() or notify() call inside a synchronized block?
  • Is that synchronized block locking the same object you're calling the method on?

Two mismatches that look fine at a glance but blow up at runtime:

// Mistake 1: synchronized on 'this' but calling wait() on a field
public synchronized void bad() throws InterruptedException {
    queue.wait(); // lock on 'this', calling wait() on 'queue' โ€” MISMATCH
}

// Mistake 2: synchronized block on the wrong object
synchronized (this) {
    queue.notify(); // holds lock on 'this', not 'queue' โ€” MISMATCH
}

Whatever object you synchronized on is the only object you can call wait() or notify() on. Same object, both roles โ€” no exceptions.

Fix It

Option 1: Synchronized method (lock on this)

Declare the method synchronized โ€” that makes this the monitor โ€” then call this.wait() and this.notify():

public class SharedQueue {
    private final List<String> queue = new ArrayList<>();

    public synchronized void waitForItem() throws InterruptedException {
        while (queue.isEmpty()) {
            this.wait(); // current thread holds 'this' monitor โ€” OK
        }
    }

    public synchronized void addItem(String item) {
        queue.add(item);
        this.notify(); // current thread holds 'this' monitor โ€” OK
    }
}

Option 2: Synchronized block on the target object

For finer-grained control, synchronize explicitly on the exact object you want to wait on:

public class SharedQueue {
    private final List<String> queue = new ArrayList<>();

    public void waitForItem() throws InterruptedException {
        synchronized (queue) {       // acquire lock on 'queue'
            while (queue.isEmpty()) {
                queue.wait();        // same object โ€” OK
            }
        }
    }

    public void addItem(String item) {
        synchronized (queue) {       // same lock
            queue.add(item);
            queue.notify();          // same object โ€” OK
        }
    }
}

Always use while, not if

One more thing โ€” separate from the lock mismatch, but just as important. Wrap wait() in a while loop, not an if. The JVM can wake a waiting thread for no real reason, a phenomenon called a spurious wakeup. With if, the thread continues without rechecking whether the condition is actually true:

// WRONG: spurious wakeup bypasses the condition check
synchronized (lock) {
    if (!ready) lock.wait();
    // 'ready' might still be false here
}

// CORRECT: recheck every time
synchronized (lock) {
    while (!ready) lock.wait();
    // 'ready' is guaranteed true here
}

Option 3: Use java.util.concurrent instead

Raw Object.wait() works, but it's easy to get wrong. Most production Java code reaches for ReentrantLock with Condition โ€” the API makes lock ownership explicit, so the kind of mismatch above is much harder to introduce by accident:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class SharedQueue {
    private final List<String> queue = new ArrayList<>();
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();

    public void waitForItem() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // equivalent to wait()
            }
        } finally {
            lock.unlock();
        }
    }

    public void addItem(String item) {
        lock.lock();
        try {
            queue.add(item);
            notEmpty.signal(); // equivalent to notify()
        } finally {
            lock.unlock();
        }
    }
}

Or skip manual lock management entirely. LinkedBlockingQueue handles all the synchronization internally โ€” no wait(), no notify(), no mismatch possible:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

BlockingQueue<String> queue = new LinkedBlockingQueue<>();

// Producer thread
queue.put("item"); // blocks if full, no manual sync needed

// Consumer thread
String item = queue.take(); // blocks until item available, no manual sync needed

Verify the Fix

Rerun the code path that was crashing. The IllegalMonitorStateException should be gone. A minimal test to confirm:

public static void main(String[] args) throws InterruptedException {
    SharedQueue sq = new SharedQueue();

    Thread consumer = new Thread(() -> {
        try {
            sq.waitForItem();
            System.out.println("Consumer: got item");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });

    Thread producer = new Thread(() -> {
        try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        sq.addItem("hello");
        System.out.println("Producer: sent item");
    });

    consumer.start();
    producer.start();
    consumer.join();
    producer.join();
}
// Expected output (order may vary):
// Producer: sent item
// Consumer: got item

Both lines print, no exception โ€” you're done.

Lessons Learned

  • wait(), notify(), and notifyAll() require a lock. Specifically, the lock on the exact object you're calling them on. The JVM checks this at runtime every single call โ€” there's no grace period.
  • The lock object and the wait/notify target must be identical. Synchronizing on this while calling field.wait() compiles without complaint and crashes at runtime. No warning, no compile error.
  • Loop on wait(), always. Spurious wakeups are a documented JVM behavior, not a theoretical edge case. An if check gives no protection; a while loop does.
  • For new code, use java.util.concurrent. BlockingQueue, ReentrantLock, and Semaphore make the locking protocol visible in the code itself. The class of bug that causes this exception becomes much harder to write by accident.

Related Error Notes