2016-06-07 1 views
1

Отправляясь на примере производитель потребительской проблемы здесь, http://java67.blogspot.com/2012/12/producer-consumer-problem-with-wait-and-notify-example.htmlВ примере Как порядок вызова для уведомленияВсе влияет на выполнение на Java?

Я вижу, что notifyall вызывается после вызова, чтобы добавить в классе производителя и до вызова для удаления, в потребительском классе. Почему это так?

Каков результат, если оба находятся в одном порядке?

Попытка понять синхронизацию.

ответ

2

Короткий ответ, Banthar is correct (+1 от меня). Независимо от того, в какой порядок вы их введете, уведомление не будет отправлено до тех пор, пока уведомляющий поток не покинет синхронизированный блок. См. the Java Language Specification (добавлено мной):

Нет гарантии, какая нить в наборе ожидания выбрана. Это удаление из набора ожидания позволяет возобновить u в действии ожидания. Обратите внимание, однако, что действия блокировки u при возобновлении не могут быть успешными до тех пор, пока t полностью не разблокирует монитор для m.

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

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

Предварительное изъятие может произойти в любое время, и последствие будет заключаться в том, что если есть промежуток между временем, в течение которого ваш поток проверяет что-либо, и временем, которое оно воздействует на результаты этой проверки, когда ваша нить не удерживала блокировку, результат проверки устарел, и ваши действия могут быть недействительными. Когда примерный код проверяет размер очереди, он получает блокировку в очереди, в промежуток времени между этой проверкой и временем, которое занимает блокировка, другой поток мог действовать, и размер очереди мог быть изменен. Вот почему Nicolas is giving you the advice to handle the locking like this:

synchronized (sharedQueue) { 
    while (sharedQueue.isEmpty()) { 
     System.out.println("Queue is empty " + Thread.currentThread().getName() 
           + " is waiting , size: " + sharedQueue.size()); 
     sharedQueue.wait(); 
    } 
} 

Что здесь происходит, что поток получает общую блокировку, и только затем проверяет, если очередь пуста. Если очередь пуста, для потока нечего делать, поэтому он освобождает блокировку и переходит в режим ожидания, пока не будет уведомлен. Затем, когда он получает уведомление, он просыпается, восстанавливает блокировку (она должна сделать это до выхода из метода ожидания) и продолжает проверять, снова ли очередь снова в верхней части цикла while. Он должен снова проверить (с заблокированной задержкой), поскольку он не может предположить, что очередь должна быть пустой на основании того, что она была уведомлена: другой поток мог залезть и действовать в промежуток времени между уведомлением об ожидающем потоке и когда он приобрел замок.

Если возникает вопрос, почему код вызывает notifyAll и не уведомляет об этом, это связано с тем, что уведомление только вызывает выбор одного потока, и этот поток может быть либо потребителем, либо производителем и, возможно, не сможет эффективно сделать что-либо , в зависимости от ситуации. Таким образом, вместо этого код пробуждает все потоки, которые пытаются получить блокировку, и позволяет им бороться с этим. Не самая эффективная вещь, но она работает.

В итоге:

  • Действуя на основе чеков, когда блокировка не удерживается подвержен ошибкам.

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

  • Вы не можете предположить, что какой-либо конкретный поток выбран для уведомления.

Дополнительную информацию см. На сайте the Oracle tutorial.

Большое изображение, потоки не должны отвечать за принятие решений о блокировке, поскольку это небезопасно: введение одной плохой нити может повредить структуру данных для всех. Структуры данных должны защищать себя от повреждения из-за одновременного доступа. Оберните свою очередь в потоковом объекте, который заботится обо всех блокировках, ожиданиях и уведомлениях.Например, если я ограниченные очереди, которая инкапсулирует список:

class MyBoundedQueue<T> { 

    private final Object lock = new Object(); 
    private final int upperBound; 

    public List mylist = new ArrayList(); 

    public MyBoundedQueue(int upperBound) { 
     this.upperBound = upperBound; 
    } 

    public T take() throws InterruptedException { 
     synchronized(lock) { 
      while (mylist.isEmpty()) { 
       lock.wait(); 
      } 
      lock.notifyAll(); 
      return mylist.remove(0); 
     } 
    } 

    public void put(T t) throws InterruptedException { 
     synchronized(lock) { 
      while (mylist.size() == upperBound) { 
       lock.wait(); 
      } 
      mylist.add(t); 
      lock.notifyAll(); 
     } 
    } 
} 

то потребитель становится проще (мы можем удалить потреблять метод), став

class Consumer implements Runnable { 
    private MyBoundedQueue<Integer> sharedQueue; 

    public Consumer(MyBoundedQueue<Integer> sharedQueue) { 
     this.sharedQueue = sharedQueue; 
    } 

    public void run() { 
     try { 
      while (!Thread.currentThread().isInterrupted()) { 
       System.out.println("consumed: " + sharedQueue.take()); 
       Thread.sleep(50L);       
      } 
     } catch (InterruptedException e) { 
      Thread.currentThread().interrupt(); 
     } 
    } 
} 

и также продюсер будет выглядеть

class Producer implements Runnable { 
    private MyBoundedQueue<Integer> sharedQueue; 

    public Producer(MyBoundedQueue<Integer> sharedQueue) { 
     this.sharedQueue = sharedQueue; 
    } 

    public void run() { 
     try { 
      for (int i = 0; i < 7; i++) { 
       sharedQueue.put(i); 
      } 
     } catch (InterruptedException e) { 
      Thread.currentThread().interrupt(); 
     } 
    } 
} 

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

+0

отличный ответ. ваш последний абзац немного запутан, так как различие между «нитями, делающими блокировку» и «объектами, делающими блокировку», является своего рода туманным. Я предполагаю, что вы пытаетесь сделать вывод о внешней блокировке и внутренней блокировке (т. е. сохранить блокировку, инкапсулированную внутри владельца структуры данных). – jtahlborn

+0

@jahlborn: большое спасибо. я попытаюсь переписать последний абзац, чтобы сделать его более ясным и добавить пример. –

3

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

1

Я вижу, что notifyall вызывается после вызова для добавления в класс производителя и перед вызовом для удаления в потребительском классе. Почему это так?

Каков результат, если оба находятся в одном порядке?

На самом деле это делается так, потому что я предполагаю, что парень, который написал это (плохой/неправильный/некрасивый ...) код неправильно хотел избежать Producer, чтобы попасть в synchronized блок внутри время цикла если Consumer удаляет элемент непосредственно перед тестированием, если очередь заполнена. Это та же самая идея для Consumer, которая ожидает, что очередь не будет пустой, если Producer производит что-то непосредственно перед Consumer, если очередь пуста, она может избежать блока .

NB: Код в этом блоге совершенно неправильно условие всегда должно быть проверено в synchronized блоке иначе вы можете получить проблемы состояние гонки так вместо того, чтобы это:

while (sharedQueue.isEmpty()) { 
    synchronized (sharedQueue) { 
     System.out.println("Queue is empty " + Thread.currentThread().getName() 
           + " is waiting , size: " + sharedQueue.size()); 
     sharedQueue.wait(); 
    } 
} 

Единственным допустимым код:

synchronized (sharedQueue) { 
    while (sharedQueue.isEmpty()) { 
     System.out.println("Queue is empty " + Thread.currentThread().getName() 
           + " is waiting , size: " + sharedQueue.size()); 
     sharedQueue.wait(); 
    } 
} 

Итак, теперь мы знаем, что код неверен, вы должны знать, что с правильным кодом мы не беспокоимся порядок больше, как мы будем проверять состояние в synchronized блок.

+0

Я считаю ваши комментарии ценными Николя, просто пытаясь обернуть голову вокруг ответа. –

+0

если не ясно, не стесняйтесь просить разъяснений –

+0

привет Николас, нет, я не сделал ни слова, я сделал uptote Banthar, потому что он непосредственно отвечает на вопрос. я согласен с вашим сообщением и не вижу, как оправдывается нисходящая линия, он не затрагивает вопрос OP как напрямую, хотя и может быть немного неясен для некоторых читателей. не потейте анонимные downvotes, они случаются со всеми. –