Базовая семантика ожидания и уведомления была представлена ранее, см.Условные очереди хороши: основная семантика ожидания и уведомления. В этой лекции рассказывается о правильной позе для использования ожидания, уведомления, уведомленияВсех.
Обязательно сначала прочитайте семантику, чтобы убедиться, что вы усвоили базовую семантику, а затем научитесь ее использовать.
Фундаментальный
классы, зависящие от состояния
Классы, зависящие от состояния: в классах, зависящих от состояния, есть определенные операции, которые имеют предварительные условия на основе состояния. То есть,Операция будет продолжена только в том случае, если состояние удовлетворяет некоторому предварительному условию..
Например, чтобы получить элемент из пустой очереди, нужно дождаться, когда состояние очереди станет «не пустым», пока это предварительное условие не будет выполнено, операция выборки элемента останется заблокированной.
Если вы впервые понимаете концепцию классов, зависящих от состояния, легко спутать классы, зависящие от состояния, с параллельными контейнерами. На самом деле это два неравных понятия:
- Ключевое слово для параллельных контейнеров: "контейнер", который _обеспечивает различные характеристики параллелизма (включая производительность, безопасность, живучесть и т. д.), и в большинстве случаев пользователи могут напрямую использовать эти контейнеры.
- Ключевое слово для класса, зависящего от состояния: "полагаться", который обеспечивает базовую логику синхронизации состояния, часто используемую для поддержания состояния параллельных программ, таких как создание параллельных контейнеров и т. д., а также может использоваться непосредственно пользователями.
Блокируемые операции, зависящие от состояния
Ядром класса, зависящего от состояния, является операция, зависящая от состояния, наиболее часто используемой является блокирующая операция, зависящая от состояния.
Суть его в следующем:
acquire lock(on object state) // 测试前需要获取锁,以保证测试时条件不变
while (precondition does not hold) { // pre-check防止信号丢失;re-check防止过早唤醒
release lock // 如果条件尚未满足,就释放锁,允许其他线程修改条件
wait until precondition might hold, interrupted or timeout expires
acquire lock // 再次测试前需要获取锁,以保证测试时条件不变
}
do sth // 如果条件已满足,就执行动作
release lock // 最后再释放锁
Содержание примечаний можно временно игнорировать, и оно будет объяснено по пунктам позже.
В соответствии с операцией измененного состояния:
acquire lock(on object state)
do sth, to make precondition might be hold
release lock
Основное поведение условной очереди — блокируемая зависимость от состояния.
В условной очереди предусловие — это условный предикат единицы, то есть условие (сигнал/уведомление), которого ожидает условная очередь. Большинство сценариев, в которых используются условные очереди, по существу— это класс, зависящий от состояния, который создает несколько условных предикатов на основе единичных условных предикатов..
правильная осанка
версия 1: базовая
Если несколько условных предикатов в конкретном сценарии называются «условными предикатами», то то, что создается, по-прежнему является блокирующей операцией, зависящей от состояния.
Можно считать, что условный предикат и условная очередь нацелены на одно и то же «условие», но условный предикат описывает содержание «условия», а условная очередь используется для поддержания зависимостей состояния, то есть « условия" в 4 строки
wait until".
Как только вы это поймете, синхронизация на основе условных очередей станет очень простой. В основном используйте API, предоставляемый Java, для реализации блокирующих операций, зависящих от состояния.
key point
Фундамент:
- Получить статус условного предиката в ожидающем потоке, подождать, если он не выполнен, и продолжить операцию, если он выполнен.
- Изменить условия предиката в состоянии потока уведомлений после уведомления
Замок:
- Состояние получения и изменения условных предикатов является взаимоисключающим и требует защиты от блокировки.
- После выполнения значения условного предиката необходимо следить за тем, чтобы состояние условного предиката оставалось неизменным во время операции, поэтому следует расширить диапазон блокировок ожидающего потока до начала проверки условия, затем войти в ждать, и, наконец, закончить после операции.
- При этом может быть выполнена только одна операция, которая соответствует одному переходу состояния условного предиката, поэтому диапазон блокировки потока уведомлений следует расширить, чтобы он начинался до операции и заканчивался после выдачи уведомления.
Связано с API:
- Когда поток уведомлений ожидает, поток уведомлений должен снять блокировку, удерживаемую им самим, и снова конкурировать за блокировку, когда условный предикат будет удовлетворен. Поэтому блокировки, которые мы используем в модели «уведомление-ожидание», должны быть связаны с условной очередью —В Java вся эта семантика выполняется методом wait(), поэтому пользователю не нужно явно снимать блокировку и получать блокировку..
Псевдокод
Используйте встроенную блокировку и встроенную условную очередь в совместно используемом объекте.
// 等待线程
synchronized (shared) {
if (precondition does not hold) {
shared.wait();
}
do sth;
}
// 通知线程
synchronized (shared) {
do sth, to make precondition might be hold;
shared.notify();
}
версия 2: просыпаться преждевременно
Условная очередь, предоставляемая Java (будь то встроенная условная очередь или явная условная очередь), сама по себе не поддерживает многомерные условные предикаты, поэтому, хотя мы пытаемся построить зависящий от состояния класс многомерных условных предикатов на основе встроенных в единичных условных предикатах условной очереди, на самом деле, два семантически не могут быть связаны между собой - это приводит к множеству проблем.
Возьмем в качестве примера встроенную условную очередь. Он обеспечивает семантику «ожидания» и «уведомления» для встроенного условного предиката единицы. тоже остался доволен. Независимо от вредоносного кода обычно существуют следующие причины пробуждения:
- собственный многомерный условный предикат удовлетворен (это наша самая желаемая ситуация)
- Тайм-аут (если вы не хотите ждать вечно)
- прерванный
- Удовлетворен многовариантный условный предикат, который разделяет с вами условную очередь (мы не рекомендуем этого, но это часто бывает со встроенными условными очередями)
- Если вам случится использовать объект потока s в качестве условной очереди, то, когда поток умрет, он автоматически разбудит поток, ожидающий s.
так,Когда поток возвращается из метода wait(), он должен снова проверить, выполняется ли многомерный условный предикат.. Меняется очень просто:
// 等待线程
synchronized (shared) {
while (precondition does not hold) {
shared.wait();
}
do sth;
}
С другой стороны, даже если на этот раз пробуждение происходит из-за выполнения предиката с несколькими условиями, его все равно нужно проверить снова. Не забывайте, что метод wait() завершает серию событий "освобождение блокировки->ожидание уведомления->получение уведомления->конкуренция блокировки->восстановление блокировки", хотя несколько условных предикатов были удовлетворены, когда "уведомление получено ", Однако между "получением уведомления" и "повторным получением блокировки" могут быть другие потоки, которые получили блокировку и изменили состояние мультиусловного предиката, снова сделав мультиусловный предикат неудовлетворительным.
Вышеуказанные ситуации являются «过早唤醒".
версия 3: сигнал потерян
Есть еще одна проблема, которую сложно заметить:При перепроверке использовать while-do или do-while?
По сути, это проблема «сначала проверить или сначала подождать», которая возникает в процессе ожидания потока и уведомления о запуске потока. Предположим, что используется do-while: если уведомляющий поток выдает уведомление первым, а ожидающий поток входит в ожидание, ожидающий поток никогда не проснется, т.е.信号丢失". Это потому, что уведомление об условной очереди не имеет"粘附性": если ни один поток не ожидает, когда очередь условий получит уведомление, уведомление отбрасывается.
Чтобы решить проблему потери сигнала, необходимо:Сначала проверьте, а потом ждите", используйте while-do.
версия 4: перехват сигнала
Устранены проблемы преждевременного пробуждения и потери сигнала, а перехват сигнала стал намного проще.
Перехват сигнала происходит, когда используется notify(), с notifyAll() проблем нет.
Предположим, что условные предикаты ожидающих потоков T1 и T2 различны, но имеют общую условную очередь s. В этот момент условный предикат T2 выполняется, s получает уведомление, и T1 выбирается случайным образом из T1 и T2, ожидающих s. Условный предикат T1 не выполнен, и после перепроверки он снова переходит в состояние блокировки, однако T2, условный предикат которого выполнен, не пробуждается. Из-за преждевременного пробуждения T1 сигнал T2 теряется, и мы говорим, что на T2 происходит перехват сигнала.
Замена notify() в коде потока уведомлений на notifyAll() может решить проблему перехвата сигнала.:
// 通知线程
synchronized (shared) {
do sth, to make precondition might be hold;
shared.notifyAll();
}
Однако побочный эффект notifyAll() очень велик: разбудить все потоки, ожидающие в очереди условий, одновременно, за исключением потока, который, наконец, конкурирует за блокировку, другие потоки эквивалентны недействительной конкуренции. На самом деле, использование notify() также может быть использовано, если каждый раз пробуждается правильный ожидающий поток. Метод прост:
-
Условная очередь привязана только к одному многомерному условному предикату.,который"
单进单出".
Если используется встроенная условная очередь, поскольку встроенная блокировка связана только с одной встроенной условной очередью, будет трудно удовлетворить условия единственного входа и выхода (например, очередь не пуста). и очередь не заполнена). Явные блокировки (такие как ReentrantLock) предоставляют метод Lock#newCondition(), который может создавать несколько очередей условий отображения для явной блокировки, чтобы гарантировать выполнение условия.
Короче говоря, проблема перехвата сигнала должна быть решена при разработке классов, зависящих от состояния. Если вы можете избежать перехвата сигнала, все равно используйте notify():
// 通知线程
synchronized (shared) {
do sth, to make precondition might be hold;
shared.notify();
}
final version
Принимая во внимание общую структуру, правильная поза использования условных очередей может быть сведена к следующим пунктам:
- полный замок
- пока-подождите
- Чтобы использовать уведомление, вы должны обеспечить
Наконец, дайте модель производитель-потребитель, которая была передана ранее, и уточните правильную позицию использования ожидания, уведомления, уведомленияВсе и обратитесь к подробному справочнику.Java реализует модель производитель-потребитель.
В этом примере производитель и потребитель являются друг для друга ожидающим потоком и потоком уведомлений; два условных предиката не пусты.buffer.size() > 0и неудовлетворенностьbuffer.size() < capсовместно использовать одну и ту же условную очередьBUFFER_LOCK, вам нужно использовать уведомление, чтобы избежать угназа сигнала. Упрощенный следующим образом:
public class WaitNotifyModel implements Model {
private final Object BUFFER_LOCK = new Object();
private final Queue<Task> buffer = new LinkedList<>();
...
private class ConsumerImpl extends AbstractConsumer implements Consumer, Runnable {
@Override
public void consume() throws InterruptedException {
synchronized (BUFFER_LOCK) {
while (buffer.size() == 0) {
BUFFER_LOCK.wait();
}
Task task = buffer.poll();
assert task != null;
// 固定时间范围的消费,模拟相对稳定的服务器处理过程
Thread.sleep(500 + (long) (Math.random() * 500));
System.out.println("consume: " + task.no);
BUFFER_LOCK.notifyAll();
}
}
}
private class ProducerImpl extends AbstractProducer implements Producer, Runnable {
@Override
public void produce() throws InterruptedException {
// 不定期生产,模拟随机的用户请求
Thread.sleep((long) (Math.random() * 1000));
synchronized (BUFFER_LOCK) {
while (buffer.size() == cap) {
BUFFER_LOCK.wait();
}
Task task = new Task(increTaskNo.getAndIncrement());
buffer.offer(task);
System.out.println("produce: " + task.no);
BUFFER_LOCK.notifyAll();
}
}
}
...
}
Заинтересованным читателям рекомендуется продолжить чтениеИсходный код|BlockingQueue параллельного цветка, из реализации LinkedBlockingQueue, узнайте, как обеспечить, чтобы "условная очередь была привязана только к одному многоусловному предикату", чтобы избежать перехвата сигнала, а также узнайте ""单次通知","条件通知" и другие распространенные методы оптимизации.
Суммировать
Использование условных очередей — хороший тест в параллельных интервью. Когда обезьяна встретилась в первый раз, его лицо было ошарашенным, и он ничего не ответил, а теперь, когда написал статью, понял, что совсем ничего не понял. Если в этой статье что-то не так, я надеюсь, что вы можете связаться со мной через краткую книгу или по электронной почте, заранее спасибо.
Серия копаем яму — о механизме реализации wait, notify и notifyAll я расскажу позже.
Ссылка на эту статью:Условные очереди хороши: правильная поза для использования wait, notify и notifyAll
автор:обезьяна 007
Источник:monkeysayhi.github.io
Эта статья основана наCreative Commons Attribution-ShareAlike 4.0Международное лицензионное соглашение выпущено, его можно перепечатать, вывести или использовать в коммерческих целях, но авторство и ссылка на эту статью должны быть сохранены.