Ошибка IllegalMonitorStateException: object not locked by thread before notify возникает в Java-приложении в тот момент, когда поток вызывает метод notify(), notifyAll() или wait() на объекте, монитор которого он не захватил. JVM требует, чтобы перед вызовом этих методов поток владел блокировкой объекта — то есть находился внутри блока synchronized по этому же объекту. Если правило нарушено, виртуальная машина немедленно бросает исключение, и поток завершает выполнение с ошибкой.
Проблема особенно коварна в многопоточном коде: с виду синхронизация может присутствовать, но блокироваться не тот объект, на котором вызывается notify(). В этой статье разберём механизм мониторов в Java, типичные сценарии появления исключения и рабочие способы его устранения — от классического synchronized/wait/notify до современных примитивов из пакета java.util.concurrent.
Что означает ошибка object not locked by thread before notify
Каждый объект в Java имеет ассоциированный с ним монитор (intrinsic lock). Когда поток входит в блок synchronized(obj), он захватывает монитор объекта obj. Только владелец монитора имеет право вызывать на этом объекте методы wait(), notify() и notifyAll() — таково требование спецификации JVM.
Текст исключения читается дословно: «объект не заблокирован потоком перед вызовом notify». Иными словами, JVM проверила владение монитором в момент вызова и обнаружила, что текущий поток им не владеет. Стоит понимать, что это проверка времени выполнения, а не компиляции — код скомпилируется без единого предупреждения, и ошибка проявится только при реальном запуске, часто в непредсказуемый момент.
Исключение IllegalMonitorStateException относится к unchecked-исключениям (наследник RuntimeException), поэтому компилятор не обязует его перехватывать. Это ещё одна причина, почему баг легко пропустить на этапе разработки и обнаружить уже в продакшене.
Типичные сценарии возникновения исключения
Чаще всего ошибка возникает из-за одного из нескольких характерных промахов в коде. Ниже перечислены ситуации, которые стоит проверить в первую очередь, если вы встретили это исключение в стектрейсе.
- 🔓 Вызов
notify()илиwait()вообще вне блокаsynchronized— самый частый случай у начинающих разработчиков. - 🎯 Синхронизация по одному объекту, а вызов
notify()— на другом, например блокировка поthis, а уведомление на объекте-очереди. - 🔁 Синхронизация по изменяемому полю: объект блокировки перезаписывается, и поток вызывает
notify()уже на новом экземпляре, которым не владеет. - 🧵 Использование
notify()внутриReentrantLockбезCondition— монитор объекта не захватывается, хотя блокировка формально есть.
Отдельный коварный случай — синхронизация по String-литералам или объектам Integer из кэша. Разные участки кода могут неявно разделять один и тот же объект блокировки, и логика владения монитором становится неочевидной. В многомодульных проектах это приводит к ошибкам, которые трудно воспроизвести локально.
Как правильно использовать wait и notify
Базовое правило формулируется просто: вызов wait/notify допустим только внутри synchronized-блока по тому же объекту. Поток, желающий уведомить других, обязан сначала захватить монитор объекта-уведомителя, выполнить notify() или notifyAll() и лишь затем выйти из блока, освободив монитор.
Корректный шаблон выглядит следующим образом:
synchronized (lock) {
while (!condition) {
lock.wait();
}
// обработка после пробуждения
}
synchronized (lock) {
condition = true;
lock.notifyAll();
}
Обратите внимание на цикл while вместо if вокруг wait(). Поток может проснуться ложно (spurious wakeup) или из-за notifyAll(), предназначенного не ему, поэтому условие нужно перепроверять после каждого пробуждения. Это не относится напрямую к IllegalMonitorStateException, но без такого паттерна корректно синхронизированный код всё равно будет работать с ошибками.
Пошаговая диагностика проблемы
Когда исключение уже поймано в логах, действуйте системно. Сначала найдите в стектрейсе точную строку кода, где вызван notify() или wait(), — JVM указывает класс и номер строки. Затем проследите, какой объект используется как монитор в ближайшем оборачивающем synchronized, и сравните его с объектом вызова.
☑️ Проверка кода с wait/notify
Если синхронизация внешне корректна, проверьте, не создаётся ли объект блокировки заново между захватом монитора и вызовом notify(). Поле-замок должно быть объявлено как private final Object lock = new Object(); — это защищает от случайной подмены объекта синхронизации. Также убедитесь, что метод не вызывается из лямбд или вложенных классов, где this ссылается на другой экземпляр, чем ожидалось.
⚠️ Внимание: синхронизация по публичному объекту (например, по
thisкласса, доступного извне) позволяет стороннему коду захватывать тот же монитор. Это может приводить не только к IllegalMonitorStateException, но и к взаимным блокировкам. Предпочтительный подход — приватный final-объект-замок.
Сравнение подходов к синхронизации
Классическая связка synchronized/wait/notify — не единственный способ организовать ожидание и уведомление потоков. В таблице ниже сопоставлены основные механизмы, доступные в стандартной библиотеке Java.
| Механизм | API уведомления | Риск IllegalMonitorStateException | Гибкость |
|---|---|---|---|
synchronized + wait/notify |
wait(), notify(), notifyAll() |
Высокий при нарушении владения монитором | Низкая: один набор ожидающих потоков на объект |
ReentrantLock + Condition |
await(), signal() |
Исключение возможно, но структура кода явнее | Высокая: несколько Condition на один замок |
BlockingQueue |
put(), take() (встроенная блокировка) |
Отсутствует — синхронизация скрыта внутри | Готовый паттерн producer-consumer |
CountDownLatch / Semaphore |
countDown(), release() |
Отсутствует | Ограниченные сценарии координации |
Из сравнения видно: чем выше уровень абстракции, тем меньше возможностей ошибиться с владением монитором. Для новых проектов классы из java.util.concurrent почти всегда предпочтительнее ручного управления через wait/notify.
Альтернатива: Lock и Condition
Если по какой-то причине нельзя использовать готовые блокирующие структуры, замените связку synchronized/wait/notify на ReentrantLock с объектом Condition. Здесь владение блокировкой контролируется явными вызовами lock() и unlock(), а ожидание и уведомление привязаны к конкретному условию, что делает код читабельнее и снижает риск ошибок.
lock.lock();
try {
while (!condition) {
conditionVar.await();
}
// работа после пробуждения
} finally {
lock.unlock();
}
Здесь тоже есть своё исключение при нарушении контракта: вызов await() или signal() без удержания замка приводит к тому же IllegalMonitorStateException. Однако благодаря явному lock()/unlock() и обязательному finally структура владения видна прямо в коде, и ошибку проще заметить на ревью. Пара Condition на один замок дополнительно позволяет разделять очереди ожидающих потоков — то, чего классический notify() не умеет.
Почему wait() нельзя вызывать без synchronized
Метод wait() заставляет поток освободить монитор объекта и встать в очередь ожидания. Если поток не владеет монитором, освобождать нечего — операция бессмысленна, поэтому JVM сразу бросает IllegalMonitorStateException. Аналогично notify() будит поток из очереди ожидания объекта, а управлять этой очередью разрешено только владельцу монитора — иначе возможны гонки при изменении состояния очереди.
Отладка и профилактика ошибок синхронизации
Для поиска проблемных мест в большом проекте полезен дамп потоков. Команда jstack <pid> показывает состояние всех потоков и удерживаемые ими мониторы — по этому выводу можно понять, кто владеет нужной блокировкой, а кто её ожидает. В IDE точки останова на строке вызова notify() позволяют проверить фактическое владение монитором в момент выполнения.
⚠️ Внимание: не пытайтесь «заглушить» исключение обёртыванием в
try-catchбез исправления логики. Перехваченный IllegalMonitorStateException означает, что уведомление не доставлено — ожидающий поток может зависнуть навсегда, и вы получите взаимную блокировку вместо явной ошибки.
Профилактика сводится к дисциплине: выделяйте отдельный private final объект-замок, документируйте, какие методы требуют удержания блокировки, и по возможности переносите координацию потоков на готовые классы java.util.concurrent. Статические анализаторы кода также способны пометить вызовы wait/notify вне синхронизированного контекста — имеет смысл включить соответствующие проверки в вашем инструменте анализа.
Часто задаваемые вопросы
Чем отличается notify() от notifyAll()?
notify() будит один произвольный поток из очереди ожидания объекта, а notifyAll() — все ожидающие потоки. Если очередь разделяют потоки с разными условиями пробуждения, notify() может разбудить «не того» потока, и уведомление будет потеряно. Поэтому в сомнительных случаях безопаснее notifyAll().
Можно ли поймать IllegalMonitorStateException и продолжить работу?
Технически — да, это unchecked-исключение, и его можно перехватить. Практически — это плохая идея: исключение означает, что уведомление не было доставлено, и ожидающие потоки останутся заблокированными. Правильное решение — исправить структуру синхронизации, а не маскировать симптом.
Почему ошибка возникает только иногда, а не при каждом запуске?
Многопоточные ошибки зависят от порядка планирования потоков. Если ветка с вызовом notify() обычно успевает захватить монитор корректно, а в редких случаях — нет (например, из-за гонки при инициализации объекта блокировки), исключение будет плавающим. Такие сбои воспроизводятся чаще под нагрузкой или на другом оборудовании.
Заменит ли ReentrantLock все случаи использования wait/notify?
Функционально — да: пара ReentrantLock + Condition покрывает те же сценарии и добавляет возможности вроде нескольких условий ожидания и прерываемого ожидания. Однако для простых задач ещё проще использовать готовые структуры — BlockingQueue, CountDownLatch или CompletableFuture, где синхронизация скрыта внутри и ошибиться с мониторами невозможно.
Влияет ли версия Java на поведение этой ошибки?
Контракт владения монитором зафиксирован в спецификации JVM и не меняется между версиями: вызов wait/notify без владения монитором всегда приводит к IllegalMonitorStateException. Меняться может лишь формат сообщения об ошибке и детализация стектрейса.