методы wait и notify () в Java

методы wait и notify () в Java

1. Вступление

В этой статье мы рассмотрим один из самых фундаментальных механизмов Java - синхронизацию потоков.

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

И мы разработаем простое приложение, в котором мы будем решать проблемы параллелизма с целью лучшего пониманияwait() иnotify().

2. Синхронизация потоков в Java

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

2.1. Охраняемые блоки в Java

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

Имея это в виду, мы будем использовать:

Это можно лучше понять из следующей диаграммы, которая отображает жизненный циклThread:

image

Обратите внимание, что существует множество способов управления этим жизненным циклом; однако в этой статье мы сосредоточимся только наwait() иnotify().

3. Методwait()

Проще говоря, когда мы вызываемwait() –, это заставляет текущий поток ждать, пока какой-либо другой поток не вызоветnotify() илиnotifyAll() для того же объекта.

Для этого текущий поток должен владеть монитором объекта. СогласноJavadocs, это может произойти, когда:

  • мы выполнили метод экземпляраsynchronized для данного объекта

  • мы выполнили тело блокаsynchronized для данного объекта

  • путем выполнения методовsynchronized static для объектов типаClass

Обратите внимание, что только один активный поток может владеть монитором объекта одновременно.

Этот методwait() имеет три перегруженных подписи. Давайте посмотрим на это.

3.1. wait()с

Методwait() заставляет текущий поток ждать бесконечно, пока другой поток не вызоветnotify() для этого объекта илиnotifyAll().

3.2. wait(long timeout)с

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

Обратите внимание, что вызовwait(0) аналогичен вызовуwait().

3.3. wait(long timeout, int nanos)с

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

Общий период ожидания (в наносекундах) рассчитывается как1_000_000*timeout + nanos.с

4. notify() иnotifyAll()

Методnotify() используется для пробуждения потоков, ожидающих доступа к монитору этого объекта.

Есть два способа оповещения ожидающих потоков.

4.1. notify()с

Для всех потоков, ожидающих на мониторе этого объекта (с использованием любого из методовwait()), методnotify() уведомляет любой из них о произвольном пробуждении. Выбор именно того потока, который нужно активировать, не является детерминированным и зависит от реализации.

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

4.2. notifyAll()с

Этот метод просто пробуждает все потоки, ожидающие на мониторе этого объекта.

Пробужденные нити завершатся обычным способом - как и любая другая нить.

Но прежде чем мы разрешим их выполнение, всегдаdefine a quick check for the condition required to proceed with the thread - потому что могут быть некоторые ситуации, когда поток просыпается без получения уведомления (этот сценарий обсуждается позже в примере).

5. Проблема синхронизации отправителя и получателя

Теперь, когда мы понимаем основы, давайте рассмотрим простое приложениеSender -Receiver, которое будет использовать методыwait() иnotify() для настройки синхронизации между ними:

  • Предполагается, чтоSender отправляет пакет данных наReceiver

  • Receiver не может обработать пакет данных, покаSender не закончит его отправку.

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

Давайте сначала создадим классData, который состоит из данныхpacket, которые будут отправлены изSender вReceiver.. Мы будем использоватьwait() иnotifyAll() для настройки синхронизации между ними:

public class Data {
    private String packet;

    // True if receiver should wait
    // False if sender should wait
    private boolean transfer = true;

    public synchronized void send(String packet) {
        while (!transfer) {
            try {
                wait();
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
        transfer = false;

        this.packet = packet;
        notifyAll();
    }

    public synchronized String receive() {
        while (transfer) {
            try {
                wait();
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
        transfer = true;

        notifyAll();
        return packet;
    }
}

Давайте разберемся, что здесь происходит:

  • Переменнаяpacket обозначает данные, которые передаются по сети.

  • У нас есть переменнаяbooleantransfer –, которуюSender иReceiver будут использовать для синхронизации:

    • Если эта переменнаяtrue, тоReceiver должен ждатьSender, чтобы отправить сообщение.

    • Если этоfalse, тоSender должен дождаться, покаReceiver получит сообщение.

  • Sender использует методsend() для отправки данных вReceiver:

    • Еслиtransfer равноfalse,, мы будем ждать, вызываяwait() в этом потоке

    • Но когда этоtrue, мы переключаем статус, устанавливаем наше сообщение и вызываемnotifyAll(), чтобы разбудить другие потоки, чтобы указать, что произошло значительное событие, и они могут проверить, могут ли они продолжить выполнение.

  • Точно так жеReceiver будет использовать методreceive():

    • Еслиtransfer был установлен наfalse наSender, то продолжится только он, в противном случае мы вызовемwait() в этом потоке

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

5.1. Зачем заключатьwait() в циклwhile?

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

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

5.2. Почему нам нужно синхронизировать методы send() иreceive()?

Мы поместили эти методы в методыsynchronized, чтобы обеспечить внутренние блокировки. Если поток, вызывающий методwait(), не владеет внутренней блокировкой, будет выдана ошибка.

Теперь мы создадимSender иReceiver и реализуем интерфейсRunnable на обоих, чтобы их экземпляры могли выполняться потоком.

Давайте сначала посмотрим, как будет работатьSender:

public class Sender implements Runnable {
    private Data data;

    // standard constructors

    public void run() {
        String packets[] = {
          "First packet",
          "Second packet",
          "Third packet",
          "Fourth packet",
          "End"
        };

        for (String packet : packets) {
            data.send(packet);

            // Thread.sleep() to mimic heavy server-side processing
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
    }
}

Для этогоSender:

  • Мы создаем несколько случайных пакетов данных, которые будут отправлены по сети в массивеpackets[]

  • Для каждого пакета мы просто вызываемsend()

  • Затем мы вызываемThread.sleep() со случайным интервалом, чтобы имитировать тяжелую обработку на стороне сервера.

Наконец, давайте реализуем нашReceiver:

public class Receiver implements Runnable {
    private Data load;

    // standard constructors

    public void run() {
        for(String receivedMessage = load.receive();
          !"End".equals(receivedMessage);
          receivedMessage = load.receive()) {

            System.out.println(receivedMessage);

            // ...
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
    }
}

Здесь мы просто вызываемload.receive() в цикле, пока не получим последний пакет данных“End”.

Давайте теперь посмотрим на это приложение в действии:

public static void main(String[] args) {
    Data data = new Data();
    Thread sender = new Thread(new Sender(data));
    Thread receiver = new Thread(new Receiver(data));

    sender.start();
    receiver.start();
}

Мы получим следующий вывод:

First packet
Second packet
Third packet
Fourth packet

И вот мы -we’ve received all data packets in the right, sequential order и успешно установили правильную связь между нашим отправителем и получателем.

6. Заключение

В этой статье мы обсудили некоторые основные концепции синхронизации в Java; более конкретно, мы сосредоточились на том, как мы можем использоватьwait() иnotify() для решения интересных задач синхронизации. И, наконец, мы прошли образец кода, где применили эти концепции на практике.

Прежде чем мы закончим здесь, стоит упомянуть, что все эти низкоуровневые API, такие какwait(),notify() иnotifyAll(), являются традиционными методами, которые работают хорошо, но механизмы более высокого уровня работают. часто проще и лучше - например, собственные интерфейсы JavaLock иCondition (доступны в пакетеjava.util.concurrent.locks).

Для получения дополнительной информации о пакетеjava.util.concurrent посетите нашу статьюoverview of the java.util.concurrent, аLock иCondition описаны вguide to java.util.concurrent.Locks, here.

Как всегда, доступны полные фрагменты кода, использованные в этой статьеover on GitHub.