🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.enableWait(AbstractQueuedSynchronizer.java:1619)
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1740)
at com.devinhyderabad.sync.BoundedBuffer.awaitSpace(BoundedBuffer.java:26)⚡ Quick Fix Works 80% of the time
Acquire the Condition’s own Lock before awaiting and release it in a finally block.
lock.lock();
try {
while (!spaceAvailable) {
notFull.await();
}
} finally {
lock.unlock();
}🧠 Why this Happens
Tap to expand the deep technical explanation
AQS ConditionObject.await() begins with enableWait, which asks the owning synchronizer isHeldExclusively(). await() must atomically enqueue the thread and release the lock; releasing a lock the thread does not hold would corrupt the owner bookkeeping, so the check throws a bare IllegalMonitorStateException before any queueing happens. The bare message is AQS convention — the frame line (enableWait) is the real diagnosis.
The HITEC City Parking Spot Analogy:
Trying to check a coat into a cloakroom using somebody else’s ticket stub — the attendant stops you at the counter because handing back property you never deposited would break the whole tracking system.
🔁 How to Reproduce Confirm this is your error
Create a ReentrantLock and one Condition, then call condition.await() with no lock.lock() first. Bare IMSE from enableWait/await, as captured. (Lab capture: OpenJDK Temurin 25.0.2.)
🛠️ Solutions (5 Ways to Fix)
lock, try, await, finally unlock — the canonical shape
👉 Use this when/if the await site simply forgot acquisition.
Awaiting requires the lock because await releases it internally. Wrapping acquisition and await in try/finally guarantees release even when the await is interrupted, keeping the owner bookkeeping consistent for every future waiter.
lock.lock();
try {
while (!spaceAvailable) {
notFull.await();
}
// critical section
} finally {
lock.unlock();
}Make the signalling side hold the lock too
👉 Use this when/if the producer calls signal() without acquiring the same lock.
signal() enforces the identical ownership rule and throws the same exception from a different frame. Beyond legality, holding the lock while signalling closes the missed-signal race where the waiter checks the predicate between the state change and the signal.
// producer
lock.lock();
try {
queue.add(item);
notEmpty.signal(); // legal: we hold the lock
} finally {
lock.unlock();
}Bind each Condition to the lock you actually hold
👉 Use this when/if two lock instances exist and the await uses the wrong one.
Conditions are children of one specific ReentrantLock. Holding lockA while awaiting a condition born from lockB fails the same isHeldExclusively check because lockB reports no owner. One lock per resource, all conditions derived from it, removes the mismatch class.
// WRONG: condB belongs to lockB, not the held lockA
lockA.lock();
condB.await(); // throws
// RIGHT
lockB.lock();
try { condB.await(); } finally { lockB.unlock(); }Drop down to BlockingQueue primitives
👉 Use this when/if the condition only expresses not-full/not-empty.
ArrayBlockingQueue ships with notFull and notEmpty conditions wired correctly inside AQS. Deleting bespoke condition plumbing eliminates both the exception and the subtle predicate bugs hand-rolled versions accumulate.
BlockingQueue<Slot> slots = new ArrayBlockingQueue<>(8);
slots.put(job); // awaits space, correctly
Slot done = slots.take(); // awaits availabilityDEV ONLY: assert-and-ignore the ownership failure
👉 Use this only in scratch code to see the failure repeat deterministically.
Catching the exception around await() leaves the thread neither queued nor parked, so the loop retries await forever, burning CPU while appearing idle. Ownership is structural — no amount of retry satisfies it.
// ANTI-PATTERN — do not ship
while (true) {
try {
notFull.await(); // lock never held
break;
} catch (IllegalMonitorStateException e) {
// spins forever
}
}📋 Version Notes
Same bare exception from enableWait; AQS internal line numbers differ substantially.
Behavior unchanged; frame names stable (enableWait, await).
Virtual threads may await Conditions without pinning carriers — j.u.c locks remain the recommended primitive on Loom-based services.
🛡️ How to Prevent This Next Time
Standardize the lock/try/finally/unlock template in a team lint rule, keep Conditions private next to their Lock, and route inter-thread handoff through BlockingQueue/SynchronousQueue so bespoke await sites are rare and reviewed.