Анализ метода ожидания уведомления параллельного программирования

Java задняя часть JVM исходный код

предисловие

С Новым 2018 годом.

Резюме:

  1. Как использовать уведомление об ожидании?
  2. Почему это должно быть в синхронизированном блоке?
  3. Реализуйте простую модель производитель-потребитель, используя ожидание уведомления.
  4. Основной принцип реализации

1. Как использовать уведомление об ожидании?

Сегодня мы собираемся изучить или разобрать два метода ожидания уведомления в классе Object.На самом деле, есть два метода.Эти два метода включают в себя свои перегруженные методы в сумме, а в классе Object всего 12. метод, мы можно увидеть важность этих двух методов. Давайте сначала посмотрим на код в JDK:

public final native void notify();

public final native void notifyAll();
 
public final void wait() throws InterruptedException {
    wait(0);
}

public final native void wait(long timeout) throws InterruptedException;

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);
}

Это пять методов. Три из этих методов являются нативными, то есть выполняются локальным кодом C виртуальной машины. Есть 2 перегруженных метода ожидания, которые в конечном итоге вызывают метод ожидания (длинный).

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

package cn.think.in.java.two;

import java.util.concurrent.TimeUnit;

public class WaitNotify {

  final static Object lock = new Object();

  public static void main(String[] args) {

    new Thread(new Runnable() {
      @Override
      public void run() {
        System.out.println("线程 A 等待拿锁");
        synchronized (lock) {
          try {
            System.out.println("线程 A 拿到锁了");
            TimeUnit.SECONDS.sleep(1);
            System.out.println("线程 A 开始等待并放弃锁");
            lock.wait();
            System.out.println("被通知可以继续执行 则 继续运行至结束");
          } catch (InterruptedException e) {
          }
        }
      }
    }, "线程 A").start();

    new Thread(new Runnable() {
      @Override
      public void run() {
        System.out.println("线程 B 等待锁");
        synchronized (lock) {
          System.out.println("线程 B 拿到锁了");
          try {
            TimeUnit.SECONDS.sleep(5);
          } catch (InterruptedException e) {
          }
          lock.notify();
          System.out.println("线程 B 随机通知 Lock 对象的某个线程");
        }
      }
    }, "线程 B").start();
  }


}

результат операции:

Поток A ожидает получения блокировки Поток B ожидает блокировки Поток A получает блокировку Поток A начинает ждать и отказывается от блокировки Поток B получает блокировку Поток B случайным образом уведомляет поток объекта Lock получить уведомление о том, что он может продолжать выполняться, а затем продолжить выполнение до конца

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

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

Если нет метода ожидания и метода noitfy, как мы можем заставить два потока взаимодействовать? Самый простой способ — создать цикл потока для проверки переменной флага, например:

while (value != flag) {
  Thread.sleep(1000);
}
doSomeing();

Вышеупомянутый код спит в течение периода времени, когда условия не выполняются. Цель этого состоит в том, чтобы предотвратить слишком быстрые "недействительные попытки". Этот метод, кажется, способен достичь желаемой функции, но есть следующие проблемы:

  1. Трудно обеспечить своевременность. Потому что ожидание 1000 раз вызовет разницу во времени.
  2. Трудно уменьшить накладные расходы, если своевременность обеспечена, время сна сокращается, что сильно потребляет ЦП.

Но с помощью собственного метода ожидания и уведомления в Java все легко решить. Официальное слово — механизм ожидания/уведомления. Один поток ожидает, и другой поток может уведомить этот поток, который реализует связь между потоками.

2. Почему это должно быть в синхронизированном блоке?

Обратите внимание, что использование этих двух методов должно быть в синхронизированном блоке синхронизации, и в блоке синхронизации текущего объекта, если метод ожидания или уведомления объекта B вызывается в методе объекта A, виртуальная машина будет throw IllegalMonitorStateException, недопустимое исключение монитора мониторинга, потому что монитор, удерживаемый вашим потоком, и монитор, который вы вызвали, не являются объектом.

Так почему же эти два метода должны быть в синхронизированном блоке?

Вот профессиональный термин: условия гонки. Каковы условия конкурса?

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

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

Предположим, есть 2 потока, производитель и потребитель, и у них есть свои задачи.

1.1 Производитель проверяет условия (например, кэш заполнен) -> 1.2 Производитель должен ждать 2.1 Потребитель потребляет единицу кеша -> 2.2 Состояние сбрасывается (например, кеш не заполнен) -> 2.3 Вызов notifyAll() для пробуждения производителя

Нам нужен следующий порядок: 1.1->1.2->2.1->2.2->2.3 Однако, поскольку выполнение ЦП является случайным, это может привести к тому, что 2.3 будет выполняться сначала, а 1.2 - позже, что приведет к тому, что производитель никогда не проснется!

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

3. Реализуйте простую модель производитель-потребитель, используя ожидание уведомления.

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

Простой класс кеша:


public class Queue {

  final int num;
  final List<String> list;
  boolean isFull = false;
  boolean isEmpty = true;


  public Queue(int num) {
    this.num = num;
    this.list = new ArrayList<>();
  }


  public synchronized void put(String value) {
    try {
      if (isFull) {
        System.out.println("putThread 暂停了,让出了锁");
        this.wait();
        System.out.println("putThread 被唤醒了,拿到了锁");
      }

      list.add(value);
      System.out.println("putThread 放入了" + value);
      if (list.size() >= num) {
        isFull = true;
      }
      if (isEmpty) {
        isEmpty = false;
        System.out.println("putThread 通知 getThread");
        this.notify();
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  public synchronized String get(int index) {
    try {
      if (isEmpty) {
        System.err.println("getThread 暂停了,并让出了锁");
        this.wait();
        System.err.println("getThread 被唤醒了,拿到了锁");
      }

      String value = list.get(index);
      System.err.println("getThread 获取到了" + value);
      list.remove(index);

      Random random = new Random();
      int randomInt = random.nextInt(5);
      if (randomInt == 1) {
        System.err.println("随机数等于1, 清空集合");
        list.clear();
      }

      if (getSize() < num) {
        if (getSize() == 0) {
          isEmpty = true;
        }
        if (isFull) {
          isFull = false;
          System.err.println("getThread 通知 putThread 可以添加了");
          Thread.sleep(10);
          this.notify();
        }
      }
      return value;


    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return null;
  }


  public int getSize() {
    return list.size();
  }


Тема производителя:

class PutThread implements Runnable {

  Queue queue;

  public PutThread(Queue queue) {
    this.queue = queue;
  }

  @Override
  public void run() {
    int i = 0;
    for (; ; ) {
      i++;
      queue.put(i + "号");

    }
  }
}

Потребительская нить:

class GetThread implements Runnable {

  Queue queue;

  public GetThread(Queue queue) {
    this.queue = queue;
  }

  @Override
  public void run() {
    for (; ; ) {
      for (int i = 0; i < queue.getSize(); i++) {
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        String value = queue.get(i);

      }
    }
  }
}

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

По сути, классический паттерн производитель-потребитель имеет следующие правила:

Ожидающая сторона следует следующим правилам:

  1. Получает блокировку объекта.
  2. Если условие не выполняется, вызывается метод ожидания объекта, и условие проверяется после получения уведомления.
  3. Если условия выполняются, выполняется соответствующая логика.

Соответствующий псевдокод вводится ниже:

synchroize( 对象 ){
    while(条件不满足){
      对象.wait();
    }
    对应的处理逻辑......
}

Уведомляющая сторона следует следующим правилам:

  1. Получает блокировку объекта.
  2. Изменить условия.
  3. Уведомить все потоки, ожидающие объекта.

Соответствующий псевдокод выглядит следующим образом:

synchronized(对象){
  改变条件
  对象.notifyAll();
}

4. Основной принцип реализации

Если вы знаете, как его использовать, вы должны знать, каков его принцип.

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

  1. При использовании wait, notify и notifyAll необходимо сначала заблокировать вызывающий объект.
  2. После вызова метода ожидания статус потока меняется с «Выполняется» на «Ожидание», а текущий поток помещается в очередь объекта.очередь ожидания.
  3. После вызова метода notify или notifyAll ожидающий поток все равно не вернется из ожидания.После того, как поток, которому нужно вызвать noitfy, снимает блокировку, ожидающий поток имеет возможность вернуться из ожидания.
  4. Метод notify перемещает ожидающий поток очереди ожидания из очереди ожидания вочередь синхронизации, а метод notifyAll будеточередь ожиданияВсе темы перемещены вочередь синхронизации, состояние перемещаемого потока изменится с Ожидание на Заблокировано.
  5. Предварительным условием возврата из метода ожидания является получение блокировки вызывающего объекта.

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

На рисунке описаны шаги, описанные выше:

WaitThread получает блокировку объекта, вызывает метод ожидания объекта, снимает блокировку, входит в очередь ожидания, а затем NotifyThread получает блокировку объекта, затем вызывает метод уведомления объекта, перемещает WatiThread в очередь ожидания. очередь синхронизации, и, наконец, выполняется NotifyThread. , освобождает блокировку, WaitThread снова получает блокировку и возвращается из метода ожидания для продолжения выполнения.

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

Примечание: мы видим, что на рисунке появляется слово Monitor, которое является монитором, На самом деле в комментариях JDK есть также предложение Текущий поток должен владеть монитором этого объекта, текущий поток должен владеть монитором объекта .

Если мы скомпилируем этот код с ключевым словом synchronized, мы обнаружим, что есть фрагмент кода, заключенный в директивы monitorenter и monitorexit, что и делает synchronized во время компиляции, а затем, когда байт-код выполняется, код c, соответствующий эта инструкция будет выполнена. Здесь мы должны остановиться, здесь задействованы соответствующие принципы синхронизирования, и в этой статье это обсуждаться не будет.

Ответы на ожидание noitfy находятся в коде C для виртуальной машины Java HotSpot. Но R big говорит нам не читать исходный код виртуальной машины легко, многие детали могут скрывать абстракцию, что приводит к низкой эффективности обучения. Если студентам интересно, есть 3 статьи, написанные великим богом для разбора исходного кода из HotSpot, адрес:

Одна из обучающих трилогий Java wait(), notify(): анализ исходного кода JVM,Учебная трилогия Java wait(), notify(), часть вторая: измените исходный код JVM, чтобы увидеть параметры,Третья трилогия обучения Java wait(), notify(): изменение исходного кода JVM для управления порядком захвата блокировки, И волкObject.wait/notify реализация анализа исходного кода JVM.

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

Дополнения

  1. wait(long), параметр этого метода — миллисекунды, то есть, если поток ждет указанное количество миллисекунд, он автоматически вернется в поток.
  2. wait(long, int), этот метод добавляет настройки уровня наносекунд. Алгоритм заключается в добавлении предыдущих миллисекунд к следующим наносекундам. Обратите внимание, что одна миллисекунда добавляется напрямую.
  3. После вызова метода уведомления, если есть много ожидающих потоков, исходный код JDK говорит, что один будет найден случайным образом, но исходный код JVM на самом деле ищет первый.
  4. notifyAll и notify не вступят в силу немедленно и должны дождаться, пока вызывающая сторона завершит выполнение блока синхронизированного кода и откажется от блокировки, прежде чем они вступят в силу.

Суммировать

Что ж, здесь представлены использование и основные принципы wait noitfy Я не знаю, заметили ли вы, что параллелизм тесно связан с виртуальными машинами. Следовательно, можно сказать, что процесс обучения параллелизму — это процесс обучения виртуальных машин. А читать код openjdk в виртуалке - большая головная боль, но несмотря ни на что, некрасивая невестка рано или поздно увидит своих свекровей, а код openjdk надо читать, давай! ! ! !