Руководство по java.util.concurrent.BlockingQueue

Руководство по java.util.concurrent.BlockingQueue

1. обзор

В этой статье мы рассмотрим одну из наиболее полезных конструкцийjava.util.concurrent для решения параллельной проблемы производитель-потребитель. Мы рассмотрим API интерфейсаBlockingQueue и то, как методы из этого интерфейса упрощают написание параллельных программ.

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

2. BlockingQueue Типы

Можно выделить два типаBlockingQueue:

  • неограниченная очередь - может расти почти бесконечно

  • ограниченная очередь - с максимальной установленной емкостью

2.1. Неограниченная очередь

Создать неограниченные очереди просто:

BlockingQueue blockingQueue = new LinkedBlockingDeque<>();

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

Самая важная вещь при разработке программы производитель-потребитель с использованием неограниченного BlockingQueue заключается в том, что потребители должны иметь возможность потреблять сообщения так же быстро, как производители добавляют сообщения в очередь. В противном случае память может заполниться, и мы получим исключениеOutOfMemory.

2.2. Ограниченная очередь

Второй тип очередей - это ограниченная очередь. Мы можем создать такие очереди, передавая емкость в качестве аргумента конструктору:

BlockingQueue blockingQueue = new LinkedBlockingDeque<>(10);

Здесь у нас естьblockingQueue, емкость которого равна 10. Это означает, что когда потребитель пытается добавить элемент в уже заполненную очередь, в зависимости от метода, который использовался для его добавления (offer(),add() илиput()), он блокирует пока не освободится место для вставки объекта. В противном случае операции не будут выполнены.

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

3. BlockingQueue API

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

3.1. Добавление элементов

  • add() – возвращаетtrue, если вставка прошла успешно, в противном случае выдаетIllegalStateException

  • put() – вставляет указанный элемент в очередь, при необходимости ожидая свободного места

  • offer() – возвращаетtrue, если вставка прошла успешно, иначеfalse

  • offer(E e, long timeout, TimeUnit unit) – пытается вставить элемент в очередь и ждет доступного слота в течение указанного тайм-аута

3.2. Получение элементов

  • take() - ожидает головного элемента очереди и удаляет его. Если очередь пуста, она блокирует и ждет, пока элемент станет доступным

  • poll(long timeout, TimeUnit unit) – извлекает и удаляет заголовок очереди, ожидая до указанного времени ожидания, если необходимо, чтобы элемент стал доступным. Возвращаетnull после тайм-аута __

Эти методы являются наиболее важными строительными блоками интерфейсаBlockingQueue при построении программ производитель-потребитель.

4. Многопоточный пример "производитель-потребитель"

Давайте создадим программу, состоящую из двух частей - производителя и потребителя.

Производитель создаст случайное число от 0 до 100 и поместит это число вBlockingQueue. У нас будет 4 потока-производителя, и мы будем использовать методput() для блокировки до тех пор, пока в очереди не появится свободное место.

Важно помнить, что нам нужно помешать нашим потребительским потокам ожидать появления элемента в очереди на неопределенное время.

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

Давайте посмотрим на класс производителя:

public class NumbersProducer implements Runnable {
    private BlockingQueue numbersQueue;
    private final int poisonPill;
    private final int poisonPillPerProducer;

    public NumbersProducer(BlockingQueue numbersQueue, int poisonPill, int poisonPillPerProducer) {
        this.numbersQueue = numbersQueue;
        this.poisonPill = poisonPill;
        this.poisonPillPerProducer = poisonPillPerProducer;
    }
    public void run() {
        try {
            generateNumbers();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private void generateNumbers() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            numbersQueue.put(ThreadLocalRandom.current().nextInt(100));
        }
        for (int j = 0; j < poisonPillPerProducer; j++) {
            numbersQueue.put(poisonPill);
        }
     }
}

Наш конструктор производителя принимает в качестве аргументаBlockingQueue, который используется для координации обработки между производителем и потребителем. Мы видим, что методgenerateNumbers() поместит 100 элементов в очередь. Требуется также сообщение «Ядовитая таблетка», чтобы узнать, какой тип сообщения помещается в очередь, когда выполнение будет завершено. Это сообщение нужно поместить в очередьpoisonPillPerProducer раз.

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

Это даст нам представление о внутренней работе наших потребителей:

public class NumbersConsumer implements Runnable {
    private BlockingQueue queue;
    private final int poisonPill;

    public NumbersConsumer(BlockingQueue queue, int poisonPill) {
        this.queue = queue;
        this.poisonPill = poisonPill;
    }
    public void run() {
        try {
            while (true) {
                Integer number = queue.take();
                if (number.equals(poisonPill)) {
                    return;
                }
                System.out.println(Thread.currentThread().getName() + " result: " + number);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

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

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

Мы хотим иметь 4 потока производителей, а количество потоков потребителей будет равно количеству доступных процессоров:

int BOUND = 10;
int N_PRODUCERS = 4;
int N_CONSUMERS = Runtime.getRuntime().availableProcessors();
int poisonPill = Integer.MAX_VALUE;
int poisonPillPerProducer = N_CONSUMERS / N_PRODUCERS;
int mod = N_CONSUMERS % N_PRODUCERS;

BlockingQueue queue = new LinkedBlockingQueue<>(BOUND);

for (int i = 1; i < N_PRODUCERS; i++) {
    new Thread(new NumbersProducer(queue, poisonPill, poisonPillPerProducer)).start();
}

for (int j = 0; j < N_CONSUMERS; j++) {
    new Thread(new NumbersConsumer(queue, poisonPill)).start();
}

new Thread(new NumbersProducer(queue, poisonPill, poisonPillPerProducer + mod)).start();

BlockingQueue создается с помощью конструкции с емкостью. Создаем 4 производителей и N потребителей. Мы указываем для нашего сообщения о ядовитой таблеткеInteger.MAX_VALUE, потому что такое значение никогда не будет отправлено нашим производителем при нормальных рабочих условиях. Самое важное, что здесь следует отметить, это то, чтоBlockingQueue используется для координации работы между ними.

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

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

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

Реализация всех этих примеров и фрагментов кода можно найти вGitHub project - это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.