Исключение java.lang.IllegalMonitorStateException: object not locked by thread before wait возникает в тот момент, когда поток вызывает метод wait() на объекте, монитор которого он не захватил — то есть вызов выполнен вне блока synchronized по этому же объекту. JVM проверяет владение монитором в рантайме, и если текущий поток не является владельцем, выполнение прерывается именно этим сообщением.
Это одна из самых частых ошибок у тех, кто начинает работать с многопоточностью в Java и механизмом wait/notify. Хорошая новость: причина почти всегда одна и та же, а исправление сводится к правильной организации синхронизации. Ниже разберём механику ошибки, типичные сценарии и корректные паттерны кода.
Что означает эта ошибка на уровне JVM
Каждый объект в Java имеет связанный с ним монитор (intrinsic lock). Методы wait(), notify() и notifyAll() определены в классе Object, и их контракт требует: вызывающий поток обязан владеть монитором объекта, на котором вызывается метод. Владение достигается входом в блок synchronized(obj) или вызовом synchronized-метода этого объекта.
Когда поток вызывает wait() без захваченного монитора, виртуальная машина не может выполнить операцию — ведь wait() должен освободить монитор, которого у потока нет. Результат — IllegalMonitorStateException с текстом про объект, не заблокированный потоком перед ожиданием.
⚠️ Внимание: та же ошибка возникает и при вызовеnotify()/notifyAll()вне синхронизированного блока. Проверяйте не только место вызоваwait(), но и код, который будит ожидающие потоки.
Типичные сценарии возникновения
Чаще всего проблема сводится к нескольким повторяющимся паттернам. Распознав свой случай, вы сразу поймёте, что исправлять.
- 🔓 Вызов wait() без synchronized — самый частый случай: разработчик просто пишет
obj.wait()в обычном методе, забыв обернуть вызов в блок синхронизации. - 🔀 Синхронизация по другому объекту — блок
synchronized(lockA)есть, ноwait()вызывается наlockB. Мониторы разные, владения нет. - 🧵 Ожидание на объекте потока — вызов
thread.wait()вместо правильной работы черезjoin()или отдельный объект-заглушку. - 📦 Синхронизация по изменяемому полю — объект-блокировка перезаписывается между захватом монитора и вызовом
wait(), и поток оказывается «не владельцем» нового экземпляра.
Как выглядит проблемный и правильный код
Рассмотрим минимальный пример, который гарантированно бросает исключение:
public class Demo {
private final Object lock = new Object();
public void badWait() throws InterruptedException {
lock.wait(); // IllegalMonitorStateException:
// object not locked by thread before wait
}
}
Исправление — обернуть вызов в synchronized по тому же объекту. Причём проверку условия ожидания нужно делать в цикле while, а не через if: это защищает от ложных пробуждений (spurious wakeup), которые допускает спецификация JVM.
public class Demo {
private final Object lock = new Object();
private boolean ready = false;
public void goodWait() throws InterruptedException {
synchronized (lock) {
while (!ready) {
lock.wait(); // монитор захвачен — всё корректно
}
}
}
public void signalReady() {
synchronized (lock) {
ready = true;
lock.notifyAll();
}
}
}
Правило простое: wait(), notify() и notifyAll() вызываются только внутри synchronized по тому же самому объекту, а условие ожидания проверяется в цикле while.
Пошаговая диагностика в вашем проекте
Если ошибка возникает в реальном приложении, а не в учебном примере, действуйте последовательно. Сначала откройте стек-трейс и найдите строку вашего кода, где брошено исключение — верхние фреймы будут внутри Object.wait(), вам нужен первый фрейм из собственных классов.
Дальше проверьте, какой именно объект используется для синхронизации вокруг этого вызова. Объект в synchronized и объект, на котором вызван wait(), обязаны быть одним и тем же экземпляром — не просто объектами одного класса, а одной ссылкой. Особенно внимательно отнеситесь к полям, которые где-то переприсваиваются.
☑️ Проверка кода с wait/notify
Сравнение подходов к синхронизации
Механизм wait/notify — низкоуровневый инструмент. В современном коде его часто заменяют конструкциями из пакета java.util.concurrent, которые менее подвержены подобным ошибкам. Сравнение подходов:
| Подход | Риск IllegalMonitorStateException | Сложность | Когда уместен |
|---|---|---|---|
| wait/notify + synchronized | Высокий при неверном порядке вызовов | Требует дисциплины | Учебные задачи, легаси-код |
| ReentrantLock + Condition | Низкий — await() требует явного lock() | Средняя | Гибкие условия ожидания |
| BlockingQueue | Практически отсутствует | Низкая | Паттерн producer–consumer |
| CountDownLatch / Semaphore | Отсутствует | Низкая | Одноразовая синхронизация потоков |
⚠️ Внимание: при переходе наReentrantLockне забудьте освобождать блокировку в блокеfinally. Вызовcondition.await()без предварительногоlock.lock()тоже приведёт кIllegalMonitorStateException— контракт здесь аналогичный.
Особые случаи и подводные камни
Отдельного внимания заслуживает ситуация с синхронизацией по объектам-обёрткам и строкам. Строковые литералы интернируются JVM, поэтому synchronized("lock") в разных классах может неожиданно захватывать один и тот же монитор — или, наоборот, вы можете синхронизироваться по одной строке, а вызывать wait() на равной по значению, но другой ссылке. Для блокировок используйте выделенный private final Object.
Ещё один тонкий момент — вызов wait() на объекте Thread. Внутри реализации join() уже используется wait() на объекте потока, и самостоятельное вмешательство в этот механизм приводит к непредсказуемому поведению. Для ожидания завершения потока применяйте join(), а для обмена сигналами — собственный объект-заглушку.
Почему wait() освобождает монитор?
При вызове wait() поток обязан отпустить монитор объекта, чтобы другой поток смог войти в synchronized-блок и вызвать notify(). Именно поэтому владение монитором — обязательное условие: нельзя освободить то, чем не владеешь. После пробуждения поток заново конкурирует за монитор и продолжает выполнение только после его повторного захвата.
Включите в IDE инспекции для многопоточного кода — например, IntelliJ IDEA подсвечивает вызовы wait()/notify() вне синхронизированного контекста ещё до запуска программы.
FAQ: частые вопросы
Можно ли вызвать wait() в synchronized-методе?
Да. Синхронизированный метод экземпляра захватывает монитор this, поэтому this.wait() (или просто wait()) внутри него корректен. Для static synchronized методов монитором служит объект класса, и wait() будет работать с ним.
Чем IllegalMonitorStateException отличается от InterruptedException?
IllegalMonitorStateException — непроверяемое исключение, сигнализирующее об ошибке в логике синхронизации. InterruptedException — проверяемое, оно означает, что ожидающий поток был прерван извне, и это штатная ситуация, которую нужно корректно обработать.
Почему условие ожидания нужно проверять в while, а не в if?
Спецификация допускает ложные пробуждения, когда wait() завершается без notify(). Кроме того, после пробуждения условие могло снова стать ложным из-за действий другого потока. Цикл while гарантирует повторную проверку перед продолжением работы.
Ошибка возникает в коде сторонней библиотеки — что делать?
Сначала убедитесь, что причина не в вашем коде, который передаёт библиотеке некорректно используемые объекты или нарушает её контракты потокобезопасности. Если проблема внутри библиотеки — обновитесь до актуальной версии и проверьте её трекер ошибок; известные дефекты синхронизации обычно исправляются мейнтейнерами.
Заменит ли synchronized на ReentrantLock автоматически?
Нет, механическая замена не поможет: у Lock свой контракт — await() на объекте Condition требует удержания соответствующей блокировки. Нужно перепроектировать участок кода с учётом нового API, а не просто подменить ключевые слова.