wait, notify, notifyAll
Часто этот вопрос формулируется как задача Producer-сonsumer. Эту задачу и практические задачи на многопоточность вообще при возможности лучше реализовывать на высокоуровневых примитивах синхронизации. Другой подход – воспользоваться также низкоуровневой, но оптимистической блокировкой на compareAndSet. Но обычно использование notify/wait (пессимистическая блокировка) – условие этого задания, то есть требуется реализовать уже существую BlockingQueue.
Эти методы вместе с synchronized – самый низкий уровень пессимистических блокировок в Java, использующийся внутри реализации примитивов синхронизации. Еще с Java 5 в непосредственном использовании этих методов нет необходимости, но теоретические знания всё еще часто спрашивают на интервью.
Чтобы вызывать эти методы у объекта, необходимо чтобы был захвачен его монитор (т.е. нужно быть внутри synchronized-блока на этом объекте). В противном случае будет выброшено IllegalMonitorStateException . Так что для полного ответа нужно понимать, как работает monitor lock (блок synchronized ).
Вызов wait тормозит текущий поток на ожидание на этом объекте и отпускает его монитор. Исполнение продолжится, когда другой поток вызовет notify и отпустит блокировку монитора. Если на объекте ожидают несколько потоков, notify разбудит один случайный, notifyAll — все сразу.
В теории, ожидание wait может быть прервано без вызова notify , по желанию JVM (spurious wakeup). На практике это бывает крайне редко, но нужно страховаться и после вызова wait добавлять дополнительную проверку условия завершения ожидания.
Еще два нештатных случая завершения wait – прерывание потока извне и таймаут ожидания. В случае прерывания выбрасывается InterruptedException . Для таймаута нужно указать время ожидания параметрами метода wait . Значение 0 проигнорируется.
Различные проблемы реализации блокировок рассмотрены в Java Concurrency in Practice 14.1.3, 14.2. Для желающих разобраться, как блокировки работают в кишках JVM, написана статья на хабре.
Java notify что делает
Иногда при взаимодействии потоков встает вопрос о извещении одних потоков о действиях других. Например, действия одного потока зависят от результата действий другого потока, и надо как-то известить один поток, что второй поток произвел некую работу. И для подобных ситуаций у класса Object определено ряд методов:
- wait() : освобождает монитор и переводит вызывающий поток в состояние ожидания до тех пор, пока другой поток не вызовет метод notify()
- notify() : продолжает работу потока, у которого ранее был вызван метод wait()
- notifyAll() : возобновляет работу всех потоков, у которых ранее был вызван метод wait()
Все эти методы вызываются только из синхронизированного контекста — синхронизированного блока или метода.
Рассмотрим, как мы можем использовать эти методы. Возьмем стандартную задачу из прошлой темы — «Производитель-Потребитель» («Producer-Consumer»): пока производитель не произвел продукт, потребитель не может его купить. Пусть производитель должен произвести 5 товаров, соответственно потребитель должен их все купить. Но при этом одновременно на складе может находиться не более 3 товаров. Для решения этой задачи задействуем методы wait() и notify() :
public class Program < public static void main(String[] args) < Store store=new Store(); Producer producer = new Producer(store); Consumer consumer = new Consumer(store); new Thread(producer).start(); new Thread(consumer).start(); >> // Класс Магазин, хранящий произведенные товары class Store < private int product=0; public synchronized void get() < while (product<1) < try < wait(); >catch (InterruptedException e) < >> product--; System.out.println("Покупатель купил 1 товар"); System.out.println("Товаров на складе: " + product); notify(); > public synchronized void put() < while (product>=3) < try < wait(); >catch (InterruptedException e) < >> product++; System.out.println("Производитель добавил 1 товар"); System.out.println("Товаров на складе: " + product); notify(); > > // класс Производитель class Producer implements Runnable < Store store; Producer(Store store)< this.store=store; >public void run() < for (int i = 1; i < 6; i++) < store.put(); >> > // Класс Потребитель class Consumer implements Runnable < Store store; Consumer(Store store)< this.store=store; >public void run() < for (int i = 1; i < 6; i++) < store.get(); >> >
Итак, здесь определен класс магазина, потребителя и покупателя. Производитель в методе run() добавляет в объект Store с помощью его метода put() 5 товаров. Потребитель в методе run() в цикле обращается к методу get объекта Store для получения этих товаров. Оба метода Store — put и get являются синхронизированными.
Для отслеживания наличия товаров в классе Store проверяем значение переменной product . По умолчанию товара нет, поэтому переменная равна 0 . Метод get() — получение товара должен срабатывать только при наличии хотя бы одного товара. Поэтому в методе get проверяем, отсутствует ли товар:
while (product<1)
Если товар отсутсвует, вызывается метод wait() . Этот метод освобождает монитор объекта Store и блокирует выполнение метода get, пока для этого же монитора не будет вызван метод notify() .
В методе put() работает похожая логика, только теперь метод put() должен срабатывать, если в магазине не более трех товаров. Поэтому в цикле проверяется наличие товара, и если товар уже есть, то освобождаем монитор с помощью wait() и ждем вызова notify() в методе get() .
И теперь программа покажет нам другие результаты:
Производитель добавил 1 товар Товаров на складе: 1 Производитель добавил 1 товар Товаров на складе: 2 Производитель добавил 1 товар Товаров на складе: 3 Покупатель купил 1 товар Товаров на складе: 2 Покупатель купил 1 товар Товаров на складе: 1 Покупатель купил 1 товар Товаров на складе: 0 Производитель добавил 1 товар Товаров на складе: 1 Производитель добавил 1 товар Товаров на складе: 2 Покупатель купил 1 товар Товаров на складе: 1 Покупатель купил 1 товар Товаров на складе: 0
Таким образом, с помощью wait() в методе get() мы ожидаем, когда производитель добавит новый продукт. А после добавления вызываем notify() , как бы говоря, что на складе освободилось одно место, и можно еще добавлять.
А в методе put() с помощью wait() мы ожидаем освобождения места на складе. После того, как место освободится, добавляем товар и через notify() уведомляем покупателя о том, что он может забирать товар.
Не очень понимаю принцип работы методов wait и notify класса Object
В статье о нём написано следующее: Метод wait без аргументов Заставляет текущий поток ждать, пока другой поток не вызовет метод notify() или метод notifyAll() для этого объекта. Другими словами, этот метод ведет себя точно так же, как если бы он просто выполнял вызов wait(0). https://java-ru-blog.blogspot.com/2019/12/object-methods.html есть код:
public class Main < public static void main(String[] args) < ThreadB threadB = new ThreadB(); threadB.start(); synchronized (threadB)< try < threadB.wait(); >catch (InterruptedException e) < e.printStackTrace(); >> System.out.println(threadB.total); > > class ThreadB extends Thread < int total; @Override public void run() < for (int i = 0; i < 5; i ++)< total+=i; try < sleep(500); >catch (InterruptedException e) < e.printStackTrace(); >> /* synchronized (this)< notify(); >*/ > >
- Поток работает до конца даже тогда, когда метод notify() закомментирован, что противоречит тому, что написано выше.
- Если раскомментировать notify(), то не очень понятно, почему вызов этого метода находится внутри потока (каким образом поток пробудит сам себя)
Отслеживать
47.5k 17 17 золотых знаков 56 56 серебряных знаков 99 99 бронзовых знаков
задан 25 фев 2021 в 14:23
Николай Семенов Николай Семенов
794 1 1 золотой знак 10 10 серебряных знаков 38 38 бронзовых знаков
Простой вопрос. Сколько в вашей программе потоков и какой из них должен быть заблокирован методом threadB.wait(); ?
25 фев 2021 в 15:32
@tym32167, на сколько я понимаю, потока 2: main и Thread-0, блокировка происходит в главном потоке (хотя я могу ошибаться)
25 фев 2021 в 17:16
Верно. А какой из потоков дорабатывает до конца, если закомментировать notify?
25 фев 2021 в 17:21
@tym32167, это уже сложнее, конечный вывод происходит в главном потоке, но, чтобы это произошло, должен полностью пройти цикл в Thread-0, поэтому, рискну предположить, что Thread-0 ко времени вывода заканчивает свою работу полностью
25 фев 2021 в 17:42
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
Поток работает до конца даже тогда, когда метод notify() закомментирован
Документации к методу wait написано:
interrupts and spurious wakeups are possible, and this method should always be used in a loop:
synchronized (obj) < while () obj.wait(); . // Perform action appropriate to condition >
и это идиоматический способ использовать wait - всегда в цикле, и проверять условие до и после вызова wait . Т.к. у вас пример синтетический, то трудно сказать, что здесь будет условием. Для классического примера publish-subscribe на стороне подписчика условие ожидания будет пока нет входящих сообщений .
Если суммировать, то проблема в spurious wakeup, т.е. в беспричинном пробуждении. Т.е. wait может вернуть управление, даже если не было notify .
не очень понятно, почему вызов этого метода находится внутри потока
Ну это вы так написали 🙂 Обычно для ожидания не используют объекты Thread , а используют другие объекты. Часто это разделяемый между потоками объект и его же и используется для передачи уведомления. В том же примере с publish - subscribe это может быть очередь, через которую передаются уведомления.
Смотрите адекватный пример хотя бы тут
Отслеживать
ответ дан 25 фев 2021 в 15:02
Roman-Stop RU aggression in UA Roman-Stop RU aggression in UA
23.3k 1 1 золотой знак 18 18 серебряных знаков 29 29 бронзовых знаков
Премного благодарен!
27 фев 2021 в 8:11
я не супер спец в java, но я предположу, что особенностью класса Thread может быть вызов Notify когда поток прекратил выполнение. То есть, в вашем коде, сначала останавливается главный поток, потом дочерний выполняет свою работу и как он закончит, он сам вызовет notify и без вашего участия, чем разблокирует основной поток. Чтобы эту теорию проверить, попробуйте закомментировать это threadB.start(); и ваша программа должна зависнуть, так как дочерний поток никогда не завершит работу (ведь он её даже не начал), а значит он никогда не разблокирует основной поток.
Чтобы провести более чистый эксперимент, давайте лочить не объект потока, а просто какой то другой объект. Например
class Solution < public static Object syncRoot = new Object(); public static void main(String[] args) < ThreadB threadB = new ThreadB(); threadB.start(); synchronized (syncRoot) < try < syncRoot.wait(); >catch (InterruptedException e) < e.printStackTrace(); >> System.out.println(threadB.total); > > class ThreadB extends Thread < int total; @Override public void run() < for (int i = 0; i < 5; i++) < total += i; try < sleep(500); >catch (InterruptedException e) < e.printStackTrace(); >> synchronized (Solution.syncRoot) < Solution.syncRoot.notify(); >> >
Я думаю, тут, если закомментировать строку Solution.syncRoot.notify(); то программа ожидаемо зависнет.
Как работают методы wait() и notify()/notifyAll()?

Эти методы определены у класса Object и предназначены для взаимодействия потоков между собой при межпоточной синхронизации.
- wait() : освобождает монитор и переводит вызывающий поток в состояние ожидания до тех пор, пока другой поток не вызовет метод notify() / notifyAll() ;
- notify() : продолжает работу потока, у которого ранее был вызван метод wait() ;
- notifyAll() : возобновляет работу всех потоков, у которых ранее был вызван метод wait() .
Когда вызван метод wait() , поток освобождает блокировку на объекте и переходит из состояния Работающий (Running) в состояние Ожидания (Waiting). Метод notify() подаёт сигнал одному из потоков, ожидающих на объекте, чтобы перейти в состояние Работоспособный (Runnable). При этом невозможно определить, какой из ожидающих потоков должен стать работоспособным. Метод notifyAll() заставляет все ожидающие потоки для объекта вернуться в состояние Работоспособный (Runnable). Если ни один поток не находится в ожидании на методе wait() , то при вызове notify() или notifyAll() ничего не происходит.
Поток может вызвать методы wait() или notify() для определённого объекта, только если он в данный момент имеет блокировку на этот объект. wait() , notify() и notifyAll() должны вызываться только из синхронизированного кода.