Почему java.util.concurrent.ArrayBlockingQueue использует циклы while вместо 'if' для вызовов await ()? - PullRequest
13 голосов
/ 02 июня 2010

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

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

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

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, нужно разрешить возникновение исключений.

Ответы [ 5 ]

20 голосов
/ 02 июня 2010

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

Это объясняется в javadoc для метода ожидания: 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 могут подавлять ложные пробуждения, но полагаться на это, следовательно, полагаться на детали реализации и делать ваш код непереносимым.

14 голосов
/ 02 июня 2010

Почему java.util.concurrent.ArrayBlockingQueue использует циклы while вместо 'if' вокруг вызовов await ()?

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

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

Пример кода

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

Описание условий гонки

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

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

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

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

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

3 голосов
/ 03 июня 2010

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

0 голосов
/ 11 июля 2016

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

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

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

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

  • Недопустимо выводить из вашего потока, прекратив ждать, что он обязательно должен быть уведомлен; это «ложное пробуждение».

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

0 голосов
/ 02 июня 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...