🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOGException in thread "main" java.lang.IllegalMonitorStateException: current thread is not owner at java.base/java.lang.Object.wait0(Native Method) at java.base/java.lang.Object.wait(Object.java:389) at java.base/java.lang.Object.wait(Object.java:351) at com.devinhyderabad.sync.Buffer.take(Buffer.java:14)

⚡ Quick Fix Works 80% of the time

Wrap the wait/notify pair in synchronized on the very object you call them on — ownership is per-object.

synchronized (buffer) { // acquire ownership first while (items.isEmpty()) { buffer.wait(); // legal now } item = items.remove(items.size() - 1); buffer.notifyAll(); }

🧠 Why this Happens

Tap to expand the deep technical explanation

Every object carries a monitor with an owner field maintained by HotSpot. wait() must release that monitor on sleep and reacquire it on wake, which is only meaningful if this thread currently owns it — so the runtime verifies the owner bit before parking and throws when it does not match. The message names the failed check literally: the current thread is not the owner of the monitor it asked to release.

The HITEC City Parking Spot Analogy:

Handing back a rental car keys for a car you never rented — the counter checks the contract name first and refuses, because releasing property you do not hold would corrupt the ledger.

🔁 How to Reproduce Confirm this is your error

Call new Object().wait() directly in main with no synchronized block. One line, immediate throw. Add synchronized(obj) around it and the exception disappears. (Lab capture: OpenJDK Temurin 25.0.2.)

🛠️ Solutions (5 Ways to Fix)

Solution 1✓ Most common cause

Synchronize on the same monitor before wait/notify

👉 Use this when/if you keep the hand-rolled wait/notify design.

Ownership is per-object and reentrant: the thread that entered synchronized(buffer) owns buffer.monitor and may wait, notify, or notifyAll until the block exits. Mismatched objects — synchronizing on lock but waiting on queue — reproduce the exception, so the two references must be identical.

private final Object lock = new Object(); public void waitForData() throws InterruptedException { synchronized (lock) { while (!dataReady) { lock.wait(); } } } public void produce() { synchronized (lock) { dataReady = true; lock.notifyAll(); } }
Solution 2

Replace the buffer with a BlockingQueue

👉 Use this when/if the monitor only implements producer-consumer handoff.

ArrayBlockingQueue or LinkedBlockingQueue implement the wait/notify choreography internally with battle-tested AQS code. Your classes shrink to put() and take() calls, and this entire exception class disappears from the codebase.

BlockingQueue<Item> queue = new ArrayBlockingQueue<>(64); // producer queue.put(item); // consumer Item item = queue.take();
Solution 3

Switch to ReentrantLock and Condition

👉 Use this when/if you need multiple independent wait sets on one lock.

Explicit locks make ownership visible in code instead of implied by block boundaries. Each Condition created from the same ReentrantLock supports await/signal with identical ownership rules, but the compiler-checked structure of lock/try/finally/unlock makes mismatches obvious in review.

private final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); public Item take() throws InterruptedException { lock.lock(); try { while (items.isEmpty()) { notEmpty.await(); } return items.removeLast(); } finally { lock.unlock(); } }
Solution 4

Encapsulate the monitor behind a private lock object

👉 Use this when/if several call sites must coordinate but should never see the lock.

Exposing the shared object publicly invites some caller to wait on it unsynchronized. Making the monitor private and exposing only intention-revealing methods (awaitData, signalData) confines synchronization discipline to one reviewed file.

public final class DataGate { private final Object monitor = new Object(); private boolean open = false; public void awaitOpen() throws InterruptedException { synchronized (monitor) { while (!open) { monitor.wait(); } } } public void open() { synchronized (monitor) { open = true; monitor.notifyAll(); } } }
Solution 5

DEV ONLY: catch the exception and retry the wait

👉 Use this never in production — ownership never changes between retries without code changes.

Wrapping wait() in a retry loop that catches IllegalMonitorStateException burns CPU and hides the missing synchronized block. The monitor owner is determined by program structure, not time, so the tenth retry fails exactly like the first.

// ANTI-PATTERN — do not ship while (true) { try { buffer.wait(); // still no synchronized! break; } catch (IllegalMonitorStateException e) { // hopeless retry } }

📋 Version Notes

Java 8

Message reads "current thread not owner" (no word "is") — verified in JDK 8 HotSpot sources (objectMonitor.cpp, check_slow).

Java 11

Still the older wording "current thread not owner"; verified in JDK 11 HotSpot sources. Behavior otherwise unchanged.

Java 17

Wording now "current thread is not owner" — captured live on Temurin 17.0.19; the ownership check itself is unchanged.

Java 21

Same rule on virtual threads — monitor ownership is per-object regardless of thread kind.

🛡️ How to Prevent This Next Time

Adopt the rule that wait/notify/notifyAll appear only inside synchronized blocks on the identical reference, enforced by ErrorProne or review checklist; default to BlockingQueue and Condition abstractions so hand-written monitor code becomes rare enough to audit.