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

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

Общее описание Object.notifyAll()

Метод Object.notifyAll() в Java служит для пробуждения всех потоков, находящихся в состоянии ожидания на мониторе данного объекта (вызвали wait() на этом объекте). Он не принимает аргументов и не возвращает значения (сигнатура: public final void notifyAll()).

Основные свойства и поведение:

  • Требование владения монитором: вызов должен выполняться из синхронизированного блока или метода, который владеет монитором (т.е. внутри synchronized(obj) или синхронизированного метода). В противном случае будет выброшено IllegalMonitorStateException.
  • Не освобождает монитор: notifyAll только переводит ожидающие потоки в состояние «готовности»; текущий поток продолжает держать монитор до выхода из синхронизированного блока. Пробуждённые потоки будут пытаться занять монитор и продолжат выполнение только после его получения.
  • Нет аргументов и возвращаемого значения: метод не принимает параметров и имеет тип возвращаемого значения void.
  • Взаимодействие с wait(): notifyAll работает совместно с wait()/wait(long)/wait(long,int). Пробуждение не гарантирует, что поток сможет сразу продолжить выполнение; возможны ложные пробуждения, поэтому рекомендуется проверять условие ожидания в цикле while.
  • Применимость: используется при необходимости пробудить все ожидающие потоки, например, при изменении состояния, от которого зависят несколько потребителей.

Типичные исключения и поведения: IllegalMonitorStateException при вызове без владения монитором; InterruptedException может быть сгенерировано у потока при вызове wait(), но не у notifyAll.

Короткие примеры использования

Пример 1: два потока ожидают и пробуждаются notifyAll.

public class NotifyAllExample {
    private final Object lock = new Object();

    public void waiter(String name) throws InterruptedException {
        synchronized (lock) {
            System.out.println(name + ": жду");
            lock.wait();
            System.out.println(name + ": пробужден");
        }
    }

    public void notifier() {
        synchronized (lock) {
            System.out.println("notifier: вызываю notifyAll");
            lock.notifyAll();
        }
    }

    public static void main(String[] args) throws Exception {
        NotifyAllExample ex = new NotifyAllExample();
        Thread t1 = new Thread(() -> { try { ex.waiter("T1"); } catch (Exception e) { e.printStackTrace(); }});
        Thread t2 = new Thread(() -> { try { ex.waiter("T2"); } catch (Exception e) { e.printStackTrace(); }});
        t1.start();
        t2.start();
        Thread.sleep(200);
        ex.notifier();
    }
}
Вывод (пример):
T1: жду
T2: жду
notifier: вызываю notifyAll
T1: пробужден
T2: пробужден

Пример 2: IllegalMonitorStateException при вызове вне синхронизации.

public class IllegalMonitorExample {
    public static void main(String[] args) {
        Object o = new Object();
        o.notifyAll(); // ошибка времени выполнения
    }
}
Результат: java.lang.IllegalMonitorStateException

Пример 3: использование с таймаутом и проверкой условия (безопасный шаблон).

synchronized (queue) {
    while (queue.isEmpty()) {
        queue.wait(1000); // ждёт или до notifyAll
    }
    // обработка
}
Если за 1000 мс очередь осталась пуста, поток продолжит после wait и проверит условие ещё раз.

Альтернативы внутри Java

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

  • Object.notify() - пробуждает один ожидающий поток вместо всех. Предпочтителен, когда требуется пробудить ровно один потребитель и это уменьшает конкуренцию за монитор.
  • java.util.concurrent.locks.Lock и Condition - дают более явный контроль: Condition.signal() и signalAll() близки по смыслу к notify/notifyAll, но позволяют работать с пользовательскими блокировками и более гибкой политикой.
  • Высокоуровневые примитивы (CountDownLatch, CyclicBarrier, Phaser, Semaphore) - предпочтительны для типичных сценариев синхронизации (барьеры, подсчёт завершения, семафоры), часто безопаснее и удобнее.
  • BlockingQueue - в задачах продюсер-потребитель часто удобнее использовать готовые структуры из java.util.concurrent, они освобождают от ручной работы с wait/notifyAll.

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

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

  • Python: threading.Condition.notify_all(). Срабатывает при условии владения условием (например, внутри with cond:). Поведение сходно с Java, есть также Event и high-level asyncio-решения.
  • C#: Monitor.PulseAll(obj) и Monitor.Wait(obj). Требуется владение блокировкой (lock), поведение близко к Java.
  • Go: sync.Cond.Broadcast() выполняет пробуждение всех; в Go чаще используются каналы (channels) для синхронизации, что меняет модель программирования.
  • JavaScript: в браузерном JS потоков на уровне ядра нет; в Web Workers возможна синхронизация через SharedArrayBuffer и Atomics.wait/Atomics.notify, где Atomics.notify(buf, idx, count) уведомляет один или несколько ожидателей.
  • PHP: в стандартной среде потоков нет; расширение pthreads предоставляет Cond::broadcast(). Отличие - расширение не всегда доступно и не типично для веб-приложений.
  • Kotlin: JVM-версия использует те же механизмы (Object.notifyAll), есть более удобные корутины и структуры из kotlinx.coroutines (Channels, Flow), которые часто предпочтительнее.
  • Lua: в стандартной Lua нет предикатов ожидания потоков; в расширениях (Lua Lanes) присутствуют способы синхронизации, а в встраиваемых средах чаще применяются каналы/сообщения.
  • SQL: синхронизация на уровне нитей обычно не применяется; используются транзакции и блокировки на уровне БД.

Небольшие примеры:

Python:

import threading
cond = threading.Condition()

def waiter():
    with cond:
        cond.wait()
        print('Python: пробужден')

t = threading.Thread(target=waiter)
t.start()
with cond:
    cond.notify_all()
Вывод: Python: пробужден

C#:

// C# (синхронизация через Monitor)
object o = new object();
// в потоке: lock(o) { Monitor.Wait(o); }
// пробуждение: lock(o) { Monitor.PulseAll(o); }
Поведение соответствует Java Monitor.PulseAll.

Go (sync.Cond):

var cond = sync.NewCond(&sync.Mutex{})
cond.L.Lock()
// в ожидателе: cond.Wait()
cond.Broadcast() // пробуждает всех
cond.L.Unlock()
Broadcast пробуждает все ожидающие горутины.

Типичные ошибки при использовании notifyAll()

  • Вызов без владения монитором - классическая ошибка: IllegalMonitorStateException. Пример выше показывает ошибку при вызове вне synchronized.
  • Отсутствие проверки условия в цикле - использование if вместо while может привести к ложным допущениям о состоянии после пробуждения (спurious wakeups или конкуренция за монитор). Пример плохой практики:
synchronized (queue) {
    if (queue.isEmpty()) {
        queue.wait(); // при пробуждении условие может быть всё ещё ложным
    }
    // предполагается, что есть элемент - риск ошибки
}
Риск: получение NoSuchElementException или некорректная обработка состояния.
  • Потерянное уведомление - сценарий, когда notifyAll вызывается до того, как поток начал ждать. Решение: проверять условие до вызова wait и организовывать порядок выполнения при необходимости.
  • Широкое пробуждение - notifyAll пробуждает всех; при большом числе ожидающих потоков это порождает повышенную конкуренцию и накладные расходы. В некоторых случаях лучше вызывать notify или использовать более подходящую структуру (BlockingQueue).

Изменения и рекомендации в новых версиях

Метод Object.notifyAll() является частью языка давно и API самого метода изменений не претерпевал. В последних релизах JDK внимание смещается к более современным средствам синхронизации из пакета java.util.concurrent и к корутинам в экосистемах (например, Kotlin coroutines). Рекомендация - по возможности применять высокоуровневые абстракции (BlockingQueue, Lock/Condition, CountDownLatch и т.д.) для повышения ясности и безопасности кода.

Расширенные и неочевидные сценарии применения

Пример A: ограниченная очередь (producer-consumer) с несколькими производителями и потребителями. Используется wait()/notifyAll() и цикл проверки условия.

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

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

    public BoundedBuffer(int capacity) { this.capacity = capacity; }

    public void put(T item) throws InterruptedException {
        synchronized (q) {
            while (q.size() == capacity) {
                q.wait();
            }
            q.addLast(item);
            q.notifyAll(); // пробуждаем потребителей
        }
    }

    public T take() throws InterruptedException {
        synchronized (q) {
            while (q.isEmpty()) {
                q.wait();
            }
            T item = q.removeFirst();
            q.notifyAll(); // пробуждаем производителей
            return item;
        }
    }
}
Поведение: несколько производителей и потребителей безопасно взаимодействуют; notifyAll используется для пробуждения всех ожидающих сторон.

Пример B: реализация барьера с помощью notifyAll.

Пример java
public class SimpleBarrier {
    private final int parties;
    private int arrived = 0;

    public SimpleBarrier(int parties) { this.parties = parties; }

    public synchronized void await() throws InterruptedException {
        arrived++;
        if (arrived < parties) {
            while (arrived < parties) {
                wait();
            }
        } else {
            arrived = 0; // сброс для повторного использования
            notifyAll();
        }
    }
}
Использование: потоки ждут, пока не соберётся заданное число участников; последний участник вызывает notifyAll и все продолжают.

Пример C: комбинирование ReentrantLock и Condition (альтернатива notifyAll) с явным сигналом.

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

public class ConditionExample {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition cond = lock.newCondition();
    private boolean ready = false;

    public void waiter() throws InterruptedException {
        lock.lock();
        try {
            while (!ready) cond.await();
            // работа
        } finally {
            lock.unlock();
        }
    }

    public void notifierAll() {
        lock.lock();
        try {
            ready = true;
            cond.signalAll();
        } finally {
            lock.unlock();
        }
    }
}
Преимущество: явное отделение блокировки и условия, более гибкое управление и ясные контракты.

Пример D: демонстрация спонтанного пробуждения и обработки InterruptedException.

Пример java
synchronized (monitor) {
    while (!condition) {
        try {
            monitor.wait();
        } catch (InterruptedException e) {
            // восстановление флага прерывания и/или логирование
            Thread.currentThread().interrupt();
            throw e;
        }
    }
}
Рекомендация: корректно обрабатывать InterruptedException, сохранять/восстанавливать флаг прерывания по необходимости.

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

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