Пожалуйста, прекратите использовать ожидание и уведомление!

Java
Пожалуйста, прекратите использовать ожидание и уведомление!

Conditionпредоставляется в JDK 1.5 для заменыwaitа такжеnotifyМетод связи нить, то кто-то должен спросить:почему бы не использоватьwaitа такжеnotifyуже?Чувак, я хорошо им пользуюсь. Не волнуйся, приятель, послушай, я расскажу тебе подробно...

Почему рекомендуется использоватьConditionвместоObjectсерединаwaitа такжеnotifyЕсть две причины:

  1. использоватьnotifyВ экстремальных условиях это заставит нить «симулировать смерть»;
  2. ConditionБолее высокая производительность.

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

1.Сообщить ветку "притворная смерть"

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

Взяв в качестве примера классическую модель производителя и потребителя в многопоточном программировании, давайте сначала продемонстрируем проблему «подвешенной смерти» потоков.

1.1 Обычная версия

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

/**
 * 工厂类,消费者和生产者通过调用工厂类实现生产/消费
 */
class Factory {
    private int[] items = new int[1]; // 数据存储容器(为了演示方便,设置容量最多存储 1 个元素)
    private int size = 0;             // 实际存储大小

    /**
     * 生产方法
     */
    public synchronized void put() throws InterruptedException {
        // 循环生产数据
        do {
            while (size == items.length) { // 注意不能是 if 判断
                // 存储的容量已经满了,阻塞等待消费者消费之后唤醒
                System.out.println(Thread.currentThread().getName() + " 进入阻塞");
                this.wait();
                System.out.println(Thread.currentThread().getName() + " 被唤醒");
            }
            System.out.println(Thread.currentThread().getName() + " 开始工作");
            items[0] = 1; // 为了方便演示,设置固定值
            size++;
            System.out.println(Thread.currentThread().getName() + " 完成工作");
            // 当生产队列有数据之后通知唤醒消费者
            this.notify();

        } while (true);
    }

    /**
     * 消费方法
     */
    public synchronized void take() throws InterruptedException {
        // 循环消费数据
        do {
            while (size == 0) {
                // 生产者没有数据,阻塞等待
                System.out.println(Thread.currentThread().getName() + " 进入阻塞(消费者)");
                this.wait();
                System.out.println(Thread.currentThread().getName() + " 被唤醒(消费者)");
            }
            System.out.println("消费者工作~");
            size--;
            // 唤醒生产者可以添加生产了
            this.notify();
        } while (true);
    }
}

Далее создадим два потока, один из которых является вызовом производителя.putметод, другой - вызов потребителяtakeметод, код реализации выглядит следующим образом:

public class NotifyDemo {
    public static void main(String[] args) {
        // 创建工厂类
        Factory factory = new Factory();

        // 生产者
        Thread producer = new Thread(() -> {
            try {
                factory.put();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者");
        producer.start();

        // 消费者
        Thread consumer = new Thread(() -> {
            try {
                factory.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "消费者");
        consumer.start();
    }
}

Результат выполнения следующий:image.pngИз приведенных выше результатов видно, что производители и потребители попеременно выполняют задачи в цикле, и сцена очень гармонична, что является правильным результатом, который нам нужен.

1.2 Версия темы "притворная смерть"

Когда есть только один производитель и один потребитель,waitа такжеnotifyПроблем с методом не будет, но при увеличении количества производителей до двух возникнет проблема "подвешенной смерти" потока Код реализации программы следующий:

public class NotifyDemo {
    public static void main(String[] args) {
		// 创建工厂方法(工厂类的代码不变,这里不再复述)
        Factory factory = new Factory();

        // 生产者
        Thread producer = new Thread(() -> {
            try {
                factory.put();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者");
        producer.start();

        // 生产者 2
        Thread producer2 = new Thread(() -> {
            try {
                factory.put();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者2");
        producer2.start();
        
        // 消费者
        Thread consumer = new Thread(() -> {
            try {
                factory.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "消费者");
        consumer.start();
    }
}

Результат выполнения программы следующий:image.pngИз приведенных выше результатов видно, что когда мы увеличим количество производителей до 2, это вызовет проблему блокировки выполнения потока «притворной смертью».Когда производитель 2 просыпается и снова блокируется, вся программа не может продолжать выполняться. . . .

Анализ темы "притворная смерть" темы

Сначала мы отмечаем шаги выполнения вышеуказанной программы и получаем следующие результаты:image.pngКак видно из приведенного выше рисунка:Когда выполняется шаг 4, производитель находится в рабочем состоянии в это время, в то время как производитель 2 и потребитель находятся в состоянии ожидания.Правильный способ в это время должен состоять в том, чтобы разбудить потребителя для потребления, а затем разбудить производитель после того, как потребитель закончил потреблять. Продолжайте работать, но в это время производитель по ошибке будит производителя 2, и производитель 2 не имеет возможности продолжить выполнение, потому что очередь заполнена, что приводит к блокировке всей программы, блок-схема выглядит следующим образом:

image.pngПравильный поток выполнения должен быть таким:image.png

1.3 Использование условий

Чтобы решить проблему «притворной смерти» потоков, мы можем использоватьConditionДавай попробуем воплотить это в жизнь,ConditionЭто класс в пакете JUC (java.util.concurrent), и его необходимо использовать.Lockблокировка для создания,ConditionПредусмотрены три важных метода:

  • await:вести перепискуwaitметод;
  • signal:вести перепискуnotifyметод;
  • signalAll:notifyAllметод.

Conditionиспользовать иwait/notifyАналогично, сначала устанавливается блокировка, а затем в блокировке выполняются операции ожидания и пробуждения.ConditionОсновное использование заключается в следующем:

// 创建 Condition 对象
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 加锁
lock.lock();
try {
    // 业务方法....
    
    // 1.进入等待状态
    condition.await();

    // 2.唤醒操作
    condition.signal();
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    lock.unlock();
}

Немного знаний: правильное использование позы Lock

ЗапомнитьLock изlock.lock()метод не может быть помещен вtryкод, еслиlockметод вtryВнутри блока кода исключения могут создаваться другими методами, что приводит кfinallyв кодовом блоке,unlockЧтобы разблокировать разблокированный объект, он вызываетAQSизtryReleaseметод (в зависимости от конкретного класса реализации), бросаетIllegalMonitorStateExceptionаномальный.

вернуться к теме

Вернемся к теме этой статьи, если мы используемConditionДля достижения связи между потоками можно избежать «притворной смерти» программы, т.к.ConditionМожно создать несколько наборов ожидания.Взяв модели производителя и потребителя в этой статье в качестве примера, мы можем использовать два набора ожидания, один для ожидания и пробуждения потребителя, а другой для пробуждения производителя, так что будет нет Производитель будит производителя (производитель может будить только потребителя, а потребитель может будить только производителя), так что весь процесс не будет "симулировать смерть", а процесс его выполнения показан на следующем рисунке :image.pngПоняв его основной процесс, давайте посмотрим на конкретный код реализации.

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

class FactoryByCondition {
    private int[] items = new int[1]; // 数据存储容器(为了演示方便,设置容量最多存储 1 个元素)
    private int size = 0;             // 实际存储大小
    // 创建 Condition 对象
    private Lock lock = new ReentrantLock();
    // 生产者的 Condition 对象
    private Condition producerCondition = lock.newCondition();
    // 消费者的 Condition 对象
    private Condition consumerCondition = lock.newCondition();

    /**
     * 生产方法
     */
    public void put() throws InterruptedException {
        // 循环生产数据
        do {
            lock.lock();
            while (size == items.length) { // 注意不能是 if 判断
                // 生产者进入等待
                System.out.println(Thread.currentThread().getName() + " 进入阻塞");
                producerCondition.await();
                System.out.println(Thread.currentThread().getName() + " 被唤醒");
            }
            System.out.println(Thread.currentThread().getName() + " 开始工作");
            items[0] = 1; // 为了方便演示,设置固定值
            size++;
            System.out.println(Thread.currentThread().getName() + " 完成工作");
            // 唤醒消费者
            consumerCondition.signal();
            try {
            } finally {
                lock.unlock();
            }
        } while (true);
    }

    /**
     * 消费方法
     */
    public void take() throws InterruptedException {
        // 循环消费数据
        do {
            lock.lock();
            while (size == 0) {
                // 消费者阻塞等待
                consumerCondition.await();
            }
            System.out.println("消费者工作~");
            size--;
            // 唤醒生产者
            producerCondition.signal();
            try {
            } finally {
                lock.unlock();
            }
        } while (true);
    }
}

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

public class NotifyDemo {
    public static void main(String[] args) {
        FactoryByCondition factory = new FactoryByCondition();

        // 生产者
        Thread producer = new Thread(() -> {
            try {
                factory.put();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者");
        producer.start();

        // 生产者 2
        Thread producer2 = new Thread(() -> {
            try {
                factory.put();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者2");
        producer2.start();

        // 消费者
        Thread consumer = new Thread(() -> {
            try {
                factory.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "消费者");
        consumer.start();
    }
}

Результат выполнения программы показан на следующем рисунке:image.pngИз вышеприведенных результатов видно, что при использованииConditionКогда производитель, потребитель и производитель 2 будут выполняться поочередно, и результат выполнения соответствует нашим ожиданиям.

2. Проблемы с производительностью

Выше мы демонстрируемnotifyКогда это вызовет проблему «подвешенной смерти» потока, некоторые друзья должны подумать, что еслиnotifyзаменитьnotifyAllНить не будет "симулировать смерть".

Такой подход действительно может решить проблему «подвешенной смерти» потоков, но при этом появятся новые проблемы с производительностью.

Используется следующееwaitа такжеnotifyAllУлучшенный код:

/**
 * 工厂类,消费者和生产者通过调用工厂类实现生产/消费功能.
 */
class Factory {
    private int[] items = new int[1];   // 数据存储容器(为了演示方便,设置容量最多存储 1 个元素)
    private int size = 0;               // 实际存储大小

    /**
     * 生产方法
     * @throws InterruptedException
     */
    public synchronized void put() throws InterruptedException {
        // 循环生产数据
        do {
            while (size == items.length) { // 注意不能是 if 判断
                // 存储的容量已经满了,阻塞等待消费者消费之后唤醒
                System.out.println(Thread.currentThread().getName() + " 进入阻塞");
                this.wait();
                System.out.println(Thread.currentThread().getName() + " 被唤醒");
            }
            System.out.println(Thread.currentThread().getName() + " 开始工作");
            items[0] = 1; // 为了方便演示,设置固定值
            size++;
            System.out.println(Thread.currentThread().getName() + " 完成工作");
            // 唤醒所有线程
            this.notifyAll();
        } while (true);
    }

    /**
     * 消费方法
     * @throws InterruptedException
     */
    public synchronized void take() throws InterruptedException {
        // 循环消费数据
        do {
            while (size == 0) {
                // 生产者没有数据,阻塞等待
                System.out.println(Thread.currentThread().getName() + " 进入阻塞(消费者)");
                this.wait();
                System.out.println(Thread.currentThread().getName() + " 被唤醒(消费者)");
            }
            System.out.println("消费者工作~");
            size--;
            // 唤醒所有线程
            this.notifyAll();
        } while (true);
    }
}

Есть еще два производителя и один потребитель, а код реализации выглядит следующим образом:

public static void main(String[] args) {
    Factory factory = new Factory();
    // 生产者
    Thread producer = new Thread(() -> {
        try {
            factory.put();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "生产者");
    producer.start();

    // 生产者 2
    Thread producer2 = new Thread(() -> {
        try {
            factory.put();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "生产者2");
    producer2.start();

    // 消费者
    Thread consumer = new Thread(() -> {
        try {
            factory.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "消费者");
    consumer.start();
}

Результат выполнения показан на следующем рисунке:image.pngИз приведенных выше результатов видно, что:когда мы звонимnotifyAllЭто не приведет к тому, что поток "симулирует смерть", но приведет к пробуждению всех производителей, но, поскольку необходимо выполнить только одну задачу, только один из всех пробужденных производителей выполнит правильную работу, а другой ничего не делать, а затем войти в состояние ожидания.Такое поведение, несомненно, лишнее для всей программы, что только увеличит накладные расходы на планирование потоков, что приведет к снижению производительности всей программы..

НапротивCondition изawait а такжеsignalметод, даже если имеется несколько производителей, программа активирует только одного действительного производителя, как показано на следующем рисунке:image.pngПроизводитель и производитель 2 будут пробуждаться поочередно для работы по очереди, поэтому в этом исполнении нет дополнительных накладных расходов по сравнению сnotifyAllПроизводительность всей программы будет значительно улучшена.

Суммировать

В этой статье мы демонстрируем с помощью кода и блок-схемwaitМетоды иnotify/notifyAllВ использовании метода есть два основных недостатка, один из которых используется в экстремальных условиях.notifyЭто заставит программу «симулировать смерть», а другой — использоватьnotifyAllЭто приведет к снижению производительности, поэтому настоятельно рекомендуется использоватьConditionкласс для реализации.

PS: Некоторые люди могут спросить, почему бы не использовать signalAll и notifyAll Condition для сравнения производительности? И использовать signal и notifyAll для сравнения? Достаточно сказать, зачем использовать signalAll, если это можно сделать с помощью signal? Это как носить короткий рукав в помещении с отоплением в 25 градусов, зачем носить телогрейку?

Подпишитесь на официальный аккаунт «Java Chinese Community», чтобы увидеть больше галантерейных товаров, и проверьте Github, чтобы найти больше интересного:GitHub.com/VIP камень/Али…