IllegalMonitorStateException is the unchecked exception that Java throws when a thread calls wait(), notify() or notifyAll() on an object whose monitor it does not own, or releases a Lock it does not hold. The message current thread is not owner is the typical sign of the first case.
We meet the exception when we write inter-thread signaling by hand, for example a worker that waits for new orders, and forget the synchronized block or lock the wrong object. The fix is always to make the calling thread own the right monitor or lock before the call.
The following example calls notify() once outside and once inside synchronized on the same object.
Object lock = new Object();
lock.notify(); // IllegalMonitorStateException: current thread is not owner
synchronized (lock) {
lock.notify(); // works, the thread owns the monitor of lock
}
boolean owner = Thread.holdsLock(lock); // false, the block has ended
The same call fails or works depending only on whether the thread owns the monitor of lock. We look at the exception class, the four situations that cause it, the fixes for each of them and the Java concurrency classes that avoid the problem.
1. The Java IllegalMonitorStateException Class
The IllegalMonitorStateException class is in the java.lang package and has existed since Java 1.0. It extends RuntimeException, so it is an unchecked exception, and no method has to declare it in a throws clause.
The class has two constructors, IllegalMonitorStateException() without a message and IllegalMonitorStateException(String message). We never throw it ourselves in application code, because the JDK throws it from these methods.
| Method | Throws when the current thread … |
|---|---|
| Object.wait(), notify(), notifyAll() | does not own the monitor of that object |
| ReentrantLock.unlock() | does not hold the lock |
| ReentrantReadWriteLock read or write unlock() | does not hold that read or write lock |
| Condition.await(), signal(), signalAll() of a ReentrantLock | does not hold the lock that created the condition |
2. What Causes IllegalMonitorStateException
Every cause comes down to one question, namely whether the thread that makes the call owns the monitor or lock of that exact object at that moment. A thread owns an object’s monitor only while it runs inside synchronized on that object, or inside a synchronized instance method of it.

2.1. Calling wait() or notify() Outside synchronized
The most common cause is a call without any synchronized block. For example, a waiting thread and a notifying thread that call obj.wait() and obj.notify() in plain run() methods both die with the exception.
Object mailbox = new Object();
mailbox.wait(100); // IllegalMonitorStateException: current thread is not owner
The rule exists to prevent a race. Without the monitor, a thread could check a condition, get preempted, miss the notification that another thread sends in between, and wait forever after that. Requiring the monitor makes the check and the wait() one atomic step for other threads that use the same monitor.
2.2. Synchronizing on a Different Object
A synchronized block gives the thread the monitor of the object in parentheses, and of no other object. Calling notifyAll() on a second object inside the block fails in the same way.
static void notifyWrongObject(Object inbox, Object outbox) {
synchronized (inbox) {
outbox.notifyAll();
}
}
Object inbox = new Object();
Object outbox = new Object();
notifyWrongObject(inbox, outbox); // IllegalMonitorStateException
A subtle version of the bug is a lock field that is not final. In the following StatusBoard class, the code replaces the lock object inside the block, so lock.notifyAll() runs on a new object that nobody has locked.
class StatusBoard {
private Object lock = new Object();
void reset() {
synchronized (lock) {
lock = new Object();
lock.notifyAll();
}
}
}
StatusBoard board = new StatusBoard();
board.reset(); // IllegalMonitorStateException
The same mistake happens with a lambda or an anonymous class. Code inside a synchronized method that submits () -> notifyAll() to an executor runs the lambda later in a pool thread, and that thread does not own the monitor even though this is the same object.
2.3. Unlocking a ReentrantLock That the Thread Does Not Hold
The ReentrantLock class uses the same exception for a wrong unlock(). It happens when a thread unlocks without locking, unlocks one time too many or unlocks a lock that another thread holds.
ReentrantLock lock = new ReentrantLock();
lock.unlock(); // IllegalMonitorStateException
lock.lock();
lock.unlock();
lock.unlock(); // IllegalMonitorStateException, unlocked twice
A typical source of the first case is an unlock() in a finally block whose lock() call sits inside the try block. If the code before lock() throws, finally still calls unlock() on a lock the thread never took.
2.4. Using a Condition Without Its Lock
A Condition created by ReentrantLock.newCondition() follows the rule of the monitor methods. The thread must hold the lock before it calls await(), signal() or signalAll().
ReentrantLock lock = new ReentrantLock();
Condition ready = lock.newCondition();
ready.signal(); // IllegalMonitorStateException
3. Resolving IllegalMonitorStateException
Say an order service has a status board, and a delivery thread waits until an order becomes PACKED. The fixed version keeps the lock in a final field, and both the waiting and the notifying code synchronize on that field before they call wait() or notifyAll().
class OrderStatus {
private final Object lock = new Object();
private String status = "NEW";
void update(String newStatus) {
synchronized (lock) {
status = newStatus;
lock.notifyAll();
}
}
String awaitStatus(String expected) throws InterruptedException {
synchronized (lock) {
while (!status.equals(expected)) {
lock.wait();
}
return status;
}
}
}
OrderStatus order = new OrderStatus();
CompletableFuture<String> delivery = CompletableFuture.supplyAsync(() -> {
try {
return order.awaitStatus("PACKED");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "interrupted";
}
});
order.update("PACKED");
String seen = delivery.get(1, TimeUnit.SECONDS); // "PACKED"
The while loop also makes the code safe when update() runs before the delivery thread starts waiting, because the thread sees the new status and never calls wait(). The checklist for the other causes is short.
- Call wait(), notify() and notifyAll() on the same object that the synchronized block locks.
- Declare lock fields final, so no code can replace the lock object.
- Call lock() right before try and unlock() as the first line of finally.
- Call await() and signal() of a Condition only between lock() and unlock() of its lock.
For the ReentrantLock cases, the standard pattern puts lock() outside the try block, so unlock() runs only when the lock was taken.
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// update the shared state
} finally {
lock.unlock();
}
boolean stillHeld = lock.isHeldByCurrentThread(); // false
While debugging, Thread.holdsLock(obj) and ReentrantLock.isHeldByCurrentThread() tell us whether the current thread owns the monitor or lock, so an assert with one of them catches the bug at the line where it starts.
4. Best Practices
The surest way to avoid IllegalMonitorStateException is to not write wait and notify code at all. The java.util.concurrent package, available since Java 5, contains classes that do the locking inside, so our code never calls the monitor methods.
- For a producer and a consumer, a BlockingQueue blocks in put() and take() and does all signaling for us.
- To wait until several threads finish, a CountDownLatch or a CyclicBarrier replaces a hand-written counter with wait().
- For one asynchronous result, a CompletableFuture replaces a flag and notifyAll().
- When we need our own conditions, ReentrantLock with Condition gives named wait sets, and tryLock(timeout) lets a thread give up instead of blocking forever, which helps to avoid a deadlock.
The rules are the same for virtual threads. A virtual thread must own the monitor before it calls wait(), and since Java 24 the wait inside synchronized no longer pins the carrier thread, so correct monitor code needs no change. For the details of the three methods, see our guide to wait(), notify() and notifyAll().
5. IllegalMonitorStateException FAQs
The message current thread is not owner shows up first in a stack trace, and the doubts that follow concern checked status, the reason for the rule, ReentrantLock and static methods.
5.1. Is IllegalMonitorStateException a checked exception?
No. It extends RuntimeException, so the compiler does not force us to catch or declare it. It signals a programming error, so we fix the code instead of catching it.
5.2. How do we fix current thread is not owner?
We move the wait(), notify() or notifyAll() call into a synchronized block on the same object, as in section 3. If the call is already inside such a block, we check that the block locks the same object and that the lock field is final.
5.3. Why must wait() be called inside a synchronized block?
Because the condition check and the wait() call must happen without another thread changing the state in between. Holding the monitor makes the pair atomic for other threads, so a notification cannot arrive in the gap and get lost.
5.4. Can ReentrantLock throw IllegalMonitorStateException?
Yes. ReentrantLock.unlock() throws it when the current thread does not hold the lock, and the await() and signal() methods of its Condition objects throw it when they run without the lock.
5.5. Does a synchronized static method let us call wait()?
Only on the Class object. A static synchronized method locks the monitor of the class, such as Printer.class, so Printer.class.wait() works there, whereas wait() on any instance throws IllegalMonitorStateException.
6. Conclusion
IllegalMonitorStateException means the current thread called a monitor or lock method without owning that monitor or lock. The usual causes are a missing synchronized block, a block on a different or replaced object, a wrong unlock() and a Condition used without its lock.
We fix it by locking the same final object that we wait on or notify, and by keeping lock() and unlock() in the standard try and finally pattern. In new code, BlockingQueue, CountDownLatch and CompletableFuture remove the need for these calls.
7. References
- IllegalMonitorStateException Javadoc (Java 25)
- Object.wait Javadoc (Java 25)
- ReentrantLock Javadoc (Java 25)
- JLS 25, Wait Sets and Notification
Happy Learning !!