Исключение 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(), и поток оказывается «не владельцем» нового экземпляра.