Подтвердить что ты не робот

Почему java.util.concurrent.ArrayBlockingQueue использует "while" вместо "if" вокруг вызовов для ожидания()?

Я играл со своей собственной версией, используя "if", и все, кажется, работают нормально. Конечно, это будет разрушаться ужасно, если вместо signal() используется signalAll(), но если уведомление об одном только за один раз, как это может пойти не так?

Их код здесь - проверьте методы put() и take(); более простая и более точная реализация может быть видна в верхней части JavaDoc for Condition.

Соответствующая часть моей реализации ниже.

public Object get() {
    lock.lock();
    try {
        if( items.size() < 1 )
            hasItems.await();
        Object poppedValue = items.getLast();
        items.removeLast();
        hasSpace.signal();
        return poppedValue; 
    } catch (InterruptedException e) {
        e.printStackTrace();
        return null;
    } finally {
        lock.unlock();
    }
}

public void put(Object item) {
    lock.lock();
    try {
        if( items.size() >= capacity )
            hasSpace.await();
        items.addFirst(item);
        hasItems.signal();
        return;
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
}

P.S. Я знаю, что в целом, особенно в классах lib, подобных этому, нужно позволить исключениям просачиваться вверх.

4b9b3361

Ответ 1

Защита от ложных бодрствований. JVM не гарантирует вам, что единственная возможная причина, по которой нить начнет работать снова, состоит в том, что вы вызывали сигнал так, как вы планировали. Иногда это просто начинается случайно и идет (Spurious пробуждается). Поэтому вам нужно снова ждать, если условие, которое вы хотите запустить, на самом деле не так.

Это объясняется в javadoc для метода wait: http://java.sun.com/javase/6/docs/api/java/lang/Object.html#wait%28long%29

И упоминается в документах для ожидания: http://java.sun.com/javase/6/docs/api/java/util/concurrent/locks/Condition.html#await%28%29

Блокировка, связанная с этим Состояние атомарно выпущено и текущая нить становится отключенной для целей планирования потоков и лежит бездействующим, пока одна из четырех вещей бывает:

  • Некоторый другой поток вызывает метод signal() для этого условия и текущая нить выбранный как поток, который нужно пробудить; или

  • Некоторый другой поток вызывает метод signalAll() для этого условия; или

  • Некоторые другие потоки прерывают текущий поток и прерывают поддерживается подвеска нити; или

* Происходит "ложное пробуждение".

Некоторые реализации интерфейса Condition могут подавлять ложные пробуждения, но, полагаясь на это, он будет полагаться на детали реализации и делает ваш код неспортивным.

Ответ 2

Почему java.util.concurrent.ArrayBlockingQueue использует "while" вместо "if" вокруг вызовов для ожидания()?

Они используют while вместо if для защиты от классических условий гонки нитей в моделях производителей/потребителей и для защиты от гораздо более редкого случая побочных пробуждений.

Когда (например) несколько потребителей ждут определенного условия (например, очередь пуста), и условие получает уведомление, есть вероятность, что другой поток может сначала заблокировать и "украсть" элемент, добавленный в очередь. Цикл while необходим для потока, чтобы убедиться, что очередь имеет элемент, прежде чем он попытается отключить его.

Пример кода

Я написал несколько пример кода и дополнительную документацию, которая демонстрирует состояние гонки.

Описание состояния гонки

Глядя на ваш конкретный код, гонка выглядит следующим образом:

  • thread # 1, потребитель, находится в await() в цикле while, ожидая, что там будут элементы в очереди
  • поток # 2, производитель, блокируется в очереди
  • thread # 3, потребитель, заканчивает употребление последнего элемента, вызывает get(), блокирует очередь и должен ждать, пока # 2 разблокируется (это НЕ ожидание hasItems но он ждет, чтобы получить lock)
  • thread # 2 добавляет элемент в очередь и вызывает hasItems.signal(), чтобы уведомить кого-то, что там есть элемент
  • поток # 1 разбужен и идет, чтобы заблокировать очередь, должен ждать для # 2, чтобы разблокировать
  • поток # 2 разблокирует
  • поток # 3 впереди потока # 1, ожидающего блокировку, поэтому он сначала блокирует очередь, переходит в цикл while и удаляет элемент, для которого был уведомлен номер 1, а затем разблокирует
  • thread # 1 теперь можно заблокировать. Если это всего лишь оператор if, он будет идти вперед и попытаться удалить из пустого списка, который бы выбрал ArrayIndexOutOfBoundsException или что-то в этом роде.

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

Это классический вопрос, который вызывает множество повторных программистов. Исходные версии библии O'Reilly pthreads, например, имели код примера без цикла while и должны были быть переизданы.

С некоторыми потоковыми системами система легче пробуждает все условия вместо специального условия, которое было сигнализировано так: "spurious wakeup" . Петли while также защищают от этого.

Ответ 3

Еще одна проблема с вашей реализацией - потенциально потерянный "сигнал". если вы внимательно посмотрите на jdk impls, которые обрабатывают InterruptedException, некоторые из них сигнализируют перед возвратом. это потому, что поток можно выбрать для сигнализации, а затем в конечном итоге получить прерывание, и в этом случае этот сигнал будет потерян. таким образом, когда прерывается, jdk имплантирует повторный сигнал перед спасением. так как этот поток может или не может действительно получать сигнал, это может также вызвать "побочный пробуждение" (как упоминалось ранее).

Ответ 4

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

Ответ 5

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

Когда поток получает уведомление во время ожидания, он не имеет блокировки. (Он отпустил замок, когда он начал ждать.) Прежде чем он сможет покинуть метод ожидания, он должен восстановить блокировку. У него нет приоритета над любым другим потоком, связанным с блокировкой, вполне вероятно, что какой-то другой поток может попасть туда первым.

Независимо от того, какие промежуточные потоки могут изменить состояние объекта, к которому осуществляется доступ, возможно, делает уведомление несущественным. Поэтому, как только уведомленный поток успешно восстановит блокировку, он должен снова проверить условие, чтобы убедиться, что условие, о котором было уведомлено, по-прежнему истинно. Цикл while существует так, что поток может проверить условие снова, как только он заблокирован.

В дополнение к этому есть другие причины использовать цикл while для проверки присутствия условия, которое вы ожидаете:

  • Недействительно выводить из потока поток, ожидающий, что он обязательно должен получить уведомление; это "ложное пробуждение".

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