почему методы wait / notify / notifyAll не синхронизируются в Java? - PullRequest
23 голосов
/ 11 августа 2011

в Java всякий раз, когда нам нужно вызвать wait / notify / notifyAll, нам нужен доступ к монитору объекта (либо через синхронизированный метод, либо через синхронизированный блок). Поэтому мой вопрос заключается в том, почему java не использовал синхронизированные методы ожидания / уведомления, чтобы снять ограничение вызова этих методов из синхронизированного блока или методов.

В случае, если они объявлены как синхронизированные, он автоматически получит доступ к монитору.

Ответы [ 7 ]

10 голосов
/ 11 августа 2011

Для уведомлений и уведомлений все, проблема с вашей идеей состоит в том, что когда вы уведомляете, у вас также есть другие вещи, которые вы обычно делаете в том же синхронизированном блоке. Поэтому синхронизация метода уведомления ничего не купит, вам все равно понадобится блок. Аналогично, ожидание должно быть в синхронизированном блоке или методе, чтобы быть полезным, например, находиться внутри спин-блокировки, где тест должен быть синхронизирован в любом случае. Таким образом, детализация блокировки не соответствует тому, что вы предлагаете.

Вот пример, речь идет о простейшей реализации очереди, которую вы можете иметь в Java:

public class MyQueue<T> {

    private List<T> list = new ArrayList<T>();

    public T take() throws InterruptedException {
        synchronized(list) {
            while (list.size() == 0) {
                list.wait();
            }
            return list.remove(0);
        }
    }

    public void put(T object) {
        synchronized(list) {
            list.add(object);
            list.notify();
        }
    }
}

Таким образом, у вас могут быть потоки производителей, которые добавляют вещи в очередь, и потоки потребителей, которые выводят вещи. Когда поток идет, чтобы получить что-то из очереди, он должен проверить в синхронизированном блоке, что что-то есть в списке, и как только он получит уведомление, он должен снова получить блокировку и убедиться, что в списке еще есть что-то (потому что некоторые другой потребительский поток мог бы вмешаться и схватить его). Существует также феномен «ложного пробуждения»: вы не можете полагаться на то, что вас разбудили, как на достаточное доказательство того, что что-то произошло, вам нужно проверить, какое состояние вы ожидаете for действительно верно, и это должно быть сделано в синхронизированном блоке.

В обоих этих случаях необходимо выполнить проверки, связанные с ожиданием, с удерживаемой блокировкой, чтобы при выполнении кода на основе этих проверок он знал, что эти результаты в настоящее время действительны.

7 голосов
/ 11 августа 2011

Хороший вопрос. Я думаю, что комментарии в реализации JDK7 Object проливают некоторый свет на это (выделено мной):

Этот метод заставляет текущий поток (назовите его T) разместить сам в ожидании, установленном для этого объекта , а затем отказаться от любого и все заявки на синхронизацию для этого объекта .

...

Поток T затем удаляется из набора ожидания для этого обычным способом с другими потоками на право синхронизации на объект; как только он получит контроль над объектом, все его претензии синхронизации на объекте восстановлены в статус-кво анте - то есть к ситуации на момент, когда wait метод был вызван . Тема T затем возвращается из вызов метода wait . Таким образом, по возвращении из wait метод, состояние синхронизации объекта и потока T точно так же, как это было, когда метод wait был прибег.

Поэтому я думаю, что первое, на что следует обратить внимание, это то, что wait() не возвращается, пока вызывающий не закончит ожидание (очевидно). Это означает, что если бы сам wait() был синхронизирован, то вызывающая сторона продолжала бы удерживать блокировку объекта, и никто другой не смог бы wait() или notify().

Теперь, очевидно, wait() делает что-то хитрое за кулисами, чтобы заставить вызывающего абонента в любом случае утратить право владения замком на объекте, но, возможно, этот трюк не сработает (или будет значительно сложнее выполнить работу), если wait() Сам был синхронизирован.

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

Или, возможно, это было просто сделано, чтобы избежать появления логического парадокса "если wait() и notify() синхронизированы, и wait() не возвращается до тех пор, пока не будет вызван notify(), как это возможно?" успешно использоваться? "

В любом случае, это мои мысли.

2 голосов
/ 11 августа 2011

Среди всех не ошибочных кодов, которые я прочитал и написал, все они используют wait/notify в большем блоке синхронизации, включающем чтение / запись других условий

synchronized(lock)
    update condition
    lock.notify()

synchronized(lock)
    while( condition not met)
        lock.wait()

Если wait/notify сами по себе synchronized, то все правильные коды не причинят вреда (может быть небольшой штраф за производительность); это не принесет никакой пользы всем правильным кодам.

Однако, это позволило бы и поощряло намного больше неправильных кодов.

2 голосов
/ 11 августа 2011

Полагаю, причина в том, что требуется блок synchronized, состоит в том, что использование wait() или notify() в качестве единственного действия в блоке synchronized почти всегда является ошибкой.

Findbugs даже имеет предупреждение об этом, которое называется " голое уведомление ".

1 голос
/ 11 августа 2011

Модель синхронизации с ожиданием и уведомлением требует, чтобы вы сначала захватили мониторы на объекте, прежде чем приступать к выполнению какой-либо работы.Она отличается от модели взаимного исключения, которая используется синхронизированным блоком.

Модель ожидания-уведомления или взаимного сотрудничества обычно используется в сценарии производитель-потребитель, где один поток создает события, которые потребляются другим потоком.,Хорошо написанные реализации будут стремиться избегать сценариев, когда потребитель голодает или производитель перегружает потребителя слишком большим количеством событий.Чтобы избежать этого, вы должны использовать протокол ожидания-уведомления, где

  • потребитель wait s для производителя, чтобы произвести событие.
  • производитель создает событие, а notifies потребитель, а затем обычно идет в спящий режим, пока потребитель не достигнет notified.
  • когда потребитель получает уведомление о событии, он просыпается, обрабатывает событие и notifies производительчто он завершил обработку события.

В этом сценарии может быть много производителей и потребителей.Получение монитора через модель взаимного исключения в wait, notify или notifyAll обязательно разрушит эту модель, поскольку производитель и потребитель не выполняют ожидание явно.Основные потоки будут присутствовать либо в наборе ожидания (используемом моделью ожидания-уведомления), либо в наборе записей (используемом моделью взаимного исключения) монитора.Вызов notify или notifyAll сигнализирует о том, что поток (ы) должны быть перемещены из набора ожидания в набор записей монитора (где может быть конфликт для монитора, среди нескольких потоков, а не только недавно уведомленного).

Теперь, когда вы хотите автоматически получить монитор на wait, notify и notifyAll с использованием модели взаимного исключения, обычно это означает, что вам не нужно использовать ожидание.уведомить модель.Это путем логического вывода - вы, как правило, будете сигнализировать другим потокам, только после выполнения некоторой работы в одном потоке, то есть при изменении состояния.Если вы автоматически получаете монитор и вызываете notify или notifyAll, вы просто перемещаете потоки из набора ожидания в набор ввода без какого-либо промежуточного состояния в вашей программе, подразумевая, что переход не требуется.Совершенно очевидно, что авторы JVM знали об этом и не объявили методы синхронизированными.

Подробнее о наборах ожидания и наборах входов мониторов можно прочитать в книге Билла Веннера - Внутривиртуальная машина Java .

1 голос
/ 11 августа 2011

Кто-то более опытный с многопоточностью может не стесняться, но я считаю, что это устранит универсальность синхронизированных блоков.Смысл их использования заключается в синхронизации определенного объекта, который функционирует как контролируемый ресурс / семафор.Методы ожидания / уведомления затем используются для управления потоком выполнения в синхронизированном блоке.

Обратите внимание, что синхронизированные методы являются сокращением для синхронизации на this в течение действия метода (или класса).для статических методов).Синхронизация методов ожидания / уведомления сама по себе устранит точку их использования в качестве сигналов остановки / перехода между потоками.

0 голосов
/ 21 апреля 2018

Я думаю, wait без synchronized может работать хорошо в некотором сценарии.Но его нельзя использовать для сложного сценария без условия гонки , могут возникнуть "ложные пробуждения".

Код работает для очереди.

// producer
give(element){
  list.add(element)
  lock.notify()
}

// consumer
take(){
  obj = null;
  while(obj == null)
    lock.wait()
    obj = list.remove(0) // ignore error indexoutofrange
  return obj
}

Этот кодЯ не объясняю состояние общих данных, он будет игнорировать последний элемент и может не работать в состоянии mutil-thread.Без состояния расы этот список в 1, 2 может иметь совершенно разные состояния.

// consumer
take(){
  while(list.isEmpty()) // 1
    lock.wait()
  return list.remove(0) // 2
}

А теперь делает его немного более сложным и очевидным.

выполнение инструкции

  • give(element) lock.notify()->take() lock.wait() resurrected->take() list.remove(0)->rollback(element)
  • give(element) lock.notify()->take() lock.wait() resurrected->rollback(element)->take() list.remove(0)

происходят "ложные пробуждения", что также делает код непредсказуемым.

// producer
give(element){
  list.add(element)
  lock.notify()
}
rollback(element){
  list.remove(element)
}

// business code 
produce(element){
   try{
     give(element)
   }catch(Exception e){
     rollback(element) // or happen in another thread
   }
}

// consumer
take(){
  obj = null;
  while(obj == null)
    lock.wait()
    obj = list.remove(0) // ignore error indexoutofrange
  return obj
}

Ссылка Криса Смита Ссылка на insidem

...