механизм ожидания/уведомления для связи потоков Java

Java

предисловие

Связь потоков Java предназначена для связывания нескольких независимых отдельных потоков, чтобы потоки могли взаимодействовать друг с другом. Например, поток A изменяет значение объекта, а затем уведомляет поток B, чтобы поток B мог узнать значение, измененное потоком A. Это взаимодействие потоков.

механизм ожидания/уведомления

Один поток вызывает метод Object wait(), чтобы заблокировать свой поток, другой поток вызывает метод Object notify()/notifyAll(), и поток, заблокированный ожиданием(), продолжает выполняться.

метод вай/уведомления

метод иллюстрировать
wait() Текущий поток блокируется и переходит в состояние ОЖИДАНИЯ.
wait(long) При блокировке длинной установки потока поток войдет в состояние TIMED_WAITING. Если заданное время (мс) не возвращается уведомление, тайм-аут
wait(long, int) Установка времени блокировки потока наносекундного уровня
notify() Уведомляет ожидающие потоки, которые выполнили метод wait() для того же объекта и получили блокировку объекта.
notifyAll() Уведомлять все ожидающие потоки на одном и том же объекте

Условия реализации механизма ожидания/уведомления:

  • Поток, вызывающий ожидание, и поток уведомления должны иметь одну и ту же блокировку объекта.
  • Метод wait() и методы notify()/notifyAll() должны находиться в синхронизированном методе или блоке кода.

Поскольку метод ожидания/уведомления определен вjava.lang.Object, поэтому его можно использовать для любого объекта Java.

метод ожидания

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

Давайте рассмотрим случай снятия блокировки с помощью функции wait().

Сначала посмотрите на выполнение кода без метода wait():

package top.ytao.demo.thread.waitnotify;

/**
 * Created by YangTao
 */
public class WaitTest {
    
    static Object object = new Object();
    
    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (object){
                System.out.println("开始线程 A");
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("结束线程 A");
            }
        }, "线程 A").start();


        new Thread(() -> {
            try {
                Thread.sleep(500L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object){
                System.out.println("开始线程 B");

                System.out.println("结束线程 B");
            }
        }, "线程 B").start();

    }

}

Создайте два потока A и B. Во-первых, приостановите работу после создания потока B, чтобы убедиться, что печать потока B выполняется после потока A. В потоке A после получения блокировки объекта он приостанавливается на некоторое время, и это время больше, чем время ожидания потока B.

Результат выполнения:

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

После выполнения метода wait() в блоке кода синхронизации потока A блокировка объекта будет активно снята. Код потока A выглядит следующим образом:

new Thread(() -> {
    synchronized (object){
        System.out.println("开始线程 A");
        try {
            // 调用 object 对象的 wait 方法
            object.wait();
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("结束线程 A");
    }
}, "线程 A").start();

Результаты:

В то же время поток A был заблокирован и не будет печатать结束线程 A.

Метод ожидания (длинный) предназначен для установки времени ожидания.Когда время ожидания превышает установленное время ожидания, он продолжит выполнение кода после метода ожидания (длинного).

new Thread(() -> {
    synchronized (object){
        System.out.println("开始线程 A");
        try {
            object.wait(1000);
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("结束线程 A");
    }
}, "线程 A").start();

Результаты

Точно так же метод wait(long, int) такой же, как и wait(long), но это всего лишь несколько настроек времени в наносекундах.

метод уведомления

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

notify()

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

package top.ytao.demo.thread.waitnotify;

/**
 * Created by YangTao
 */
public class WaitNotifyTest {

    static Object object = new Object();

    public static void main(String[] args) {
        System.out.println();

        new Thread(() -> {
            synchronized (object){
                System.out.println("开始线程 A");
                try {
                    object.wait();
                    System.out.println("A 线程重新获取到锁,继续进行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("结束线程 A");
            }
        }, "线程 A").start();


        new Thread(() -> {
            try {
                Thread.sleep(500L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object){
                System.out.println("开始线程 B");
                object.notify();
                System.out.println("线程 B 通知完线程 A");
                try {
                    // 试验执行完 notify() 方法后,A 线程是否能立即获取到锁
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("结束线程 B");
            }
        }, "线程 B").start();

    }

}

Вышеупомянутый поток A выполняет метод wait(), поток B выполняет метод notify(), и результат выполнения таков:

Как видно в результате выполнения, резьба B выполняется после уведомления () метода, даже если спать, нить не приобрела замок. Видно, что метод Notify () не отпускает замок.

Следующий код создает два потока, которые выполняют метод wait():

package top.ytao.demo.thread.waitnotify;

/**
 * Created by YangTao
 */
public class MultiWaitNotifyTest {

    static Object object = new Object();

    public static void main(String[] args) {
        System.out.println();

        new Thread(() -> {
            synchronized (object){
                System.out.println("开始线程 A");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("结束线程 A");
            }
        }, "线程 A").start();


        new Thread(() -> {
            try {
                Thread.sleep(500L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object){
                System.out.println("开始线程 B");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("结束线程 B");
            }
        }, "线程 B").start();


        new Thread(() -> {
            try {
                Thread.sleep(3000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object){
                System.out.println("开始通知线程 C");
                object.notify();
                object.notify();
                System.out.println("结束通知线程 C");
            }
        }, "线程 C").start();

    }

}

Сначала поток A выполняет метод wait(), затем поток B выполняет метод wait(), и, наконец, поток C дважды вызывает метод notify(), и результат выполнения:

notifyAll()

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

Для реализации просто измените часть многократного вызова метода notify() в потоке C выше на однократный вызов метода notifyAll().

new Thread(() -> {
    try {
        Thread.sleep(3000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    synchronized (object){
        System.out.println("开始通知线程 C");
        object.notifyAll();
        System.out.println("结束通知线程 C");
    }
}, "线程 C").start();

Результаты:

В зависимости от реализации различных JVM последовательность пробуждения notifyAll() будет разной.В текущей тестовой среде потоки пробуждаются в обратном порядке.

Реализация шаблона «производитель-потребитель»

В производственно-потребительском режиме один поток производит данные для хранения, а другой поток извлекает и потребляет данные. Ниже представлена ​​симуляция с двумя потоками.Производитель генерирует UUID и сохраняет его в объекте List.Потребитель считывает данные в объекте List и очищает их после прочтения.

Код реализации выглядит следующим образом:

package top.ytao.demo.thread.waitnotify;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Created by YangTao
 */
public class WaitNotifyModelTest {

    // 存储生产者产生的数据
    static List<String> list = new ArrayList<>();

    public static void main(String[] args) {

        new Thread(() -> {
            while (true){
                synchronized (list){
                    // 判断 list 中是否有数据,如果有数据的话,就进入等待状态,等数据消费完
                    if (list.size() != 0){
                        try {
                            list.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    // list 中没有数据时,产生数据添加到 list 中
                    list.add(UUID.randomUUID().toString());
                    list.notify();
                    System.out.println(Thread.currentThread().getName() + list);
                }
            }
        }, "生产者线程 A ").start();


        new Thread(() -> {
            while (true){
                synchronized (list){
                    // 如果 list 中没有数据,则进入等待状态,等收到有数据通知后再继续运行
                    if (list.size() == 0){
                        try {
                            list.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    // 有数据时,读取数据
                    System.out.println(Thread.currentThread().getName() + list);
                    list.notify();
                    // 读取完毕,将当前这条 UUID 数据进行清除
                    list.clear();
                }
            }
        }, "消费者线程 B ").start();

    }

}

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

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

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

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

Выполняется простой шаблон производитель-потребитель.

Суммировать

Механизм ожидания/уведомления — это способ реализации связи между потоками Java.В многопоточности каждый независимо работающий поток взаимодействует друг с другом для более эффективного выполнения работы и более эффективного использования программы обработки ЦП. Это также необходимо знать для изучения или исследования потоков Java.

Рекомендуемое чтение

"Основы многопоточности в Java, начните с этого"

«Вы должны знать динамический прокси JDK и динамический прокси CGLIB»

"Серия Даббо"

Обратите внимание на паблик [ytao], больше оригинальных хороших статей

我的公众号