Object.notify(): примеры (JAVA)

Обзор применения Object.notify() в Java
Раздел: Многопоточность, Синхронизация
Object.notify(): void

Общее описание

Метод Object.notify() принадлежит классу java.lang.Object и используется в низкоуровневой модели синхронизации потоков. Метод пробуждает один поток, который ожидает монитора данного объекта (то есть ранее вызвал wait() на том же объекте). Выбор конкретного потока, если их несколько, выполняется по усмотрению реализации JVM.

Сигнатура метода: public final native void notify(). Метод не принимает аргументов и не возвращает значения (тип void).

Условия вызова и поведение:

  • Вызов должен выполняться при владении монитором объекта (внутри блока synchronized(obj){ ... } или эквивалентного механизма). В противном случае возникает IllegalMonitorStateException.
  • Метод помещает в состояние готовности (runnable) ровно один поток из очереди ожидания монитора. Пробужденный поток не сразу продолжит выполнение: перед возобновлением он должен вновь успешно получить монитор объекта.
  • Если в очереди ожидания нет потоков, вызов notify() просто ничего не делает.
  • Возможны спонтанные пробуждения (spurious wakeups); корректная программа использует цикл с проверкой условия при вызове wait().
  • Часто используется вместе с wait() и альтернативой notifyAll().

Исключения и прочие детали:

  • IllegalMonitorStateException - при попытке вызвать метод без владения монитором.
  • Метод является native и final; реализация зависит от JVM, но спецификация поведения описана в документации Java.

Короткие примеры

Пример 1 - минимальная схема wait/notify для пробуждения одного потока.

class Example1 {
    private final Object lock = new Object();
    private boolean ready = false;

    void waiter() throws InterruptedException {
        synchronized (lock) {
            while (!ready) {
                lock.wait();
            }
            System.out.println("Поток пробужден, готов = " + ready);
        }
    }

    void notifier() {
        synchronized (lock) {
            ready = true;
            lock.notify();
        }
    }

    public static void main(String[] args) throws Exception {
        Example1 ex = new Example1();
        Thread t = new Thread(() -> {
            try { ex.waiter(); } catch (InterruptedException e) { }
        });
        t.start();
        Thread.sleep(100);
        ex.notifier();
    }
}
Возможный вывод:
Поток пробужден, готов = true

Пример 2 - вызов notify без синхронизации приводит к исключению.

public class Example2 {
    public static void main(String[] args) {
        Object o = new Object();
        o.notify(); // ошибка времени выполнения
    }
}
Выполнение приводит к исключению:
Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	... (stack trace)

Пример 3 - различие notify и notifyAll (показано в упрощенном виде).

// Если несколько потоков ждут на одном объекте, notify пробудит один из них.
// notifyAll пробудит всех, и каждый будет пытаться получить монитор по очереди.
Возможный вывод с notify:
Поток A продолжил
(только один из ожидающих потоков)

При использовании notifyAll:
Поток A продолжил
Поток B продолжил
(все ожидающие потоки пробуждаются и конкурируют за монитор)

Похожие механизмы в Java

Список основных альтернатив и их отличия кратко:

  • notifyAll() - пробуждает всех потоков в очереди ожидания. Предпочтение зависит от логики: когда несколько условий или сложности с выбором потока, используется notifyAll.
  • java.util.concurrent.locks.Condition (в связке с ReentrantLock) - более гибкая замена: поддерживает несколько условных очередей, явную блокировку/разблокировку и явные методы signal() и signalAll().
  • Высокоуровневые структуры (BlockingQueue, Semaphore, CountDownLatch, CyclicBarrier) - часто предпочтительнее для типовых задач синхронизации, так как устраняют классы ошибок и упрощают код.
  • LockSupport.park/unpark - низкоуровневые примитивы для управления блокировкой потоков; дают больше контроля, но требуют аккуратной работы.

Для большинства сценариев обмена данными между потоками рекомендуются BlockingQueue или Condition вместо прямого использования wait/notify, так как они уменьшают риск ошибок.

Аналоги в других языках

Краткие соответствия и отличия:

  • Python: threading.Condition с методами notify() и notify_all(). Отличие: Python-реализация позволяет указать число пробуждаемых потоков в notify(n). Пример:
import threading
cond = threading.Condition()
ready = False

def waiter():
    with cond:
        while not ready:
            cond.wait()
        print('Пробуждено в Python')

threading.Thread(target=waiter).start()
with cond:
    ready = True
    cond.notify()
Вывод:
Пробуждено в Python
  • JavaScript: отсутствие общих нитей в браузере. Для общей памяти в worker-окружении есть Atomics.wait и Atomics.notify для SharedArrayBuffer. Отличие: API работает с числовыми ячейками общей памяти и блокируется только в средах, поддерживающих блокирующие ожидания.
// Пример упрощенный (не выполняется в обычном main-потоке браузера)
// Atomics.wait(typedArray, index, value);
// Atomics.notify(typedArray, index, count);
Результат зависит от среды: один или несколько воркеров пробуждаются.
  • C#: Monitor.Pulse(obj) и Monitor.PulseAll(obj) соответствуют notify/notifyAll. Требуется владение монитором (lock). Также доступны AutoResetEvent/ManualResetEvent и newer Task-based API.
// C#
lock(obj) {
    Monitor.Pulse(obj); // пробуждает один
}
Поведение близко к Java: пробуждается один ожидающий поток.
  • Go: предпочитаются channels для коммуникации между горутинами. Есть также sync.Cond с методами Signal() и Broadcast(), похожими на notify/notifyAll.
// Go с sync.Cond
var mu sync.Mutex
cond := sync.NewCond(&mu)

mu.Lock()
// ... условие
cond.Signal() // пробуждает один
mu.Unlock()
Поведение: один горутин пробуждается и конкурирует за блокировку.
  • Kotlin: использует те же примитивы, что JVM: Any.wait(), Any.notify(). Дополнительно доступны kotlinx.coroutines с каналами и шарт.
  • PHP: в стандартном PHP нет потоков. В расширениях (pthreads) есть механизмы синхронизации: Cond::wait(), Cond::signal(). Отличие: требует установленного расширения и специфической модели.
  • Lua: в чистой Lua нет потоков ОС; при использовании библиотек (Lua Lanes, LuaThreads) предоставляются свои примитивы синхронизации.
  • SQL: концепция notify отсутствует; синхронизация на уровне СУБД решается блокировками транзакций, уведомлениями (NOTIFY/LISTEN в PostgreSQL), которые логически отличаются от примитивов потоков в JVM.

Типичные ошибки

Ниже перечислены частые ошибки при работе с notify() и короткие примеры.

  • Вызов без владения монитором - приводит к IllegalMonitorStateException. Пример см. в разделе examples.
  • Потерянное уведомление - notify может сработать до того, как поток начал ждать. При таком порядке ожидание может остаться бесконечным. Решение - установить булево условие и проверять его в цикле перед wait:
// Плохо:
// Thread A: notifier() вызывает notify()
// Thread B: позже вызывает wait() и будет ждать вечно

// Правильно: использовать флаг 'ready' и цикл while
Пример проблемы: поток может зависнуть навсегда при некорректном порядке вызовов.
  • Использование notify вместо notifyAll - может привести к дедлоку, если пробуждённый поток не тот, который сможет продолжить работу (например, при нескольких типов условий на одном мониторе).
  • Неучёт спонтанных пробуждений - важно всегда применять цикл с проверкой условия вокруг wait.
  • Игнорирование InterruptedException - при ожидании потоки могут быть прерваны; в обработке прерываний следует корректно реагировать и сохранять семантику прерывания.

Изменения и примечания по версиям

Сам метод Object.notify() присутствует в Java с ранних версий и не претерпевал существенных изменений в сигнатуре или семантике. Основные примечания:

  • Метод остаётся final native и документация по его поведению стабильна.
  • В поздних версиях Java (Project Loom) появились виртуальные потоки; семантика wait/notify остается, но стратегию планирования потоков определяет JVM. Для высокоуровневой конкуренции предпочтительнее использовать современные API (CompletableFuture, structured concurrency, каналы корутин и т.д.).

Расширенные и редкие примеры

Пример A - простая блокирующая очередь на wait/notifyAll.

Пример java
import java.util.LinkedList;

class SimpleBlockingQueue {
    private final LinkedList q = new LinkedList<>();
    private final int capacity;

    SimpleBlockingQueue(int capacity) { this.capacity = capacity; }

    public synchronized void put(E item) throws InterruptedException {
        while (q.size() == capacity) {
            wait();
        }
        q.addLast(item);
        notifyAll();
    }

    public synchronized E take() throws InterruptedException {
        while (q.isEmpty()) {
            wait();
        }
        E item = q.removeFirst();
        notifyAll();
        return item;
    }
}

// Использование: несколько производителей и потребителей.
Вывод (пример):
Производитель добавил 1
Потребитель взял 1
... (взаимодействие между потоками)

Комментарий: используется notifyAll, чтобы избежать проблем выбора неверного потока при нескольких типах ожидающих потоков.

Пример B - работа с таймаутом и обработкой спонтанных пробуждений.

Пример java
synchronized (lock) {
    long deadline = System.currentTimeMillis() + timeoutMillis;
    while (!conditionSatisfied()) {
        long waitTime = deadline - System.currentTimeMillis();
        if (waitTime <= 0) break; // таймаут
        lock.wait(waitTime);
    }
}
Результат: функция вернётся либо при выполнении условия, либо по таймауту.

Пример C - корректная обработка прерываний при ожидании.

Пример java
synchronized (lock) {
    while (!done) {
        try {
            lock.wait();
        } catch (InterruptedException e) {
            // Сохранение статуса прерывания и выход или повторная установка флага
            Thread.currentThread().interrupt();
            break;
        }
    }
}
Результат: в случае прерывания поток получает InterruptedException и можно корректно выйти из ожидания.

Пример D - разделение очередей ожидания путём использования разных объектов-мониторов.

Пример java
// Для разных типов условий применяются разные lock-объекты:
private final Object lockA = new Object();
private final Object lockB = new Object();

// Потоки ждущие A используют lockA.wait(), уведомления для них - lockA.notify()
// Аналогично для B с lockB
Результат: уменьшение конкуренции и выборочного пробуждения нужных потоков.

Пример E - сочетание wait/notify и ReentrantLock/Condition для миграции на более гибкий API.

Пример java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

ReentrantLock lock = new ReentrantLock();
Condition cond = lock.newCondition();

lock.lock();
try {
    while (!ready) cond.await();
} finally { lock.unlock(); }

// Пробуждение:
lock.lock();
try {
    ready = true;
    cond.signal();
} finally { lock.unlock(); }
Результат: аналогично wait/notify, но с возможностью иметь несколько Condition и более явным управлением блокировкой.

джава Object.notify() function comments

En
Object.notify() Пробуждает один поток, ожидающий монитор