Руководство по Java TransferQueue

1. Обзор

В этой статье мы рассмотрим конструкцию TransferQueue из стандартного java.util. пакет concurrent .

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

Реализация фактически похожа на BlockingQueue , но дает нам новую возможность реализовать форму противодавления. Это означает, что, когда производитель отправляет сообщение потребителю с помощью метода transfer () , производитель остается заблокированным, пока сообщение не будет использовано.

2. Один производитель - нулевые потребители

Давайте протестируем метод transfer () из TransferQueue - ожидаемое поведение заключается в том, что производитель будет заблокирован, пока потребитель не получит сообщение из очереди с помощью метода take () .

Для этого мы создадим программу, в которой есть один производитель, но нет потребителей. Первый вызов transfer () из потока производителя будет блокироваться на неопределенный срок, так как у нас нет потребителей, которые могли бы извлечь этот элемент из очереди.

Давайте посмотрим, как выглядит класс Producer :

class Producer implements Runnable {
    private TransferQueue<String> transferQueue;

    private String name;

    private Integer numberOfMessagesToProduce;

    public AtomicInteger numberOfProducedMessages
      = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < numberOfMessagesToProduce; i++) {
            try {
                boolean added
                  = transferQueue.tryTransfer("A" + i, 4000, TimeUnit.MILLISECONDS);
                if(added){
                    numberOfProducedMessages.incrementAndGet();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
   //standard constructors
}

Мы передаем экземпляр TransferQueue конструктору вместе с именем, которое мы хотим дать нашему производителю, и количеством элементов, которые следует перенести в очередь.

Обратите внимание, что мы используем метод tryTransfer () с заданным временем ожидания.

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

Теперь давайте посмотрим на класс Consumer :

class Consumer implements Runnable {

    private TransferQueue<String> transferQueue;

    private String name;

    private int numberOfMessagesToConsume;

    public AtomicInteger numberOfConsumedMessages
     = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < numberOfMessagesToConsume; i++) {
            try {
                String element = transferQueue.take();
                longProcessing(element);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void longProcessing(String element)
      throws InterruptedException {
        numberOfConsumedMessages.incrementAndGet();
        Thread.sleep(500);
    }

   //standard constructors
}

Он похож на производителя, но мы получаем элементы из очереди с помощью метода take () . Мы также моделируем некоторые длительные действия, используя метод longProcessing () , в котором мы увеличиваем переменную numberOfConsumedMessages , которая является счетчиком полученных сообщений.

Теперь давайте запустим нашу программу только с одним производителем:

@Test
public void whenUseOneProducerAndNoConsumers__thenShouldFailWithTimeout()
  throws InterruptedException {
   //given
    TransferQueue<String> transferQueue = new LinkedTransferQueue<>();
    ExecutorService exService = Executors.newFixedThreadPool(2);
    Producer producer = new Producer(transferQueue, "1", 3);

   //when
    exService.execute(producer);

   //then
    exService.awaitTermination(5000, TimeUnit.MILLISECONDS);
    exService.shutdown();

    assertEquals(producer.numberOfProducedMessages.intValue(), 0);
}

Мы хотим отправить три элемента в очередь, но производитель заблокирован на первом элементе, и нет потребителя, который мог бы извлечь этот элемент из очереди _. Мы используем метод tryTransfer () _ , который будет блокироваться до появления сообщения используется или истекло время ожидания.

По истечении времени ожидания он вернет false , чтобы указать, что передача не удалась, и попытается передать следующую. Это вывод из предыдущего примера:

Producer: 1 is waiting to transfer...
can not add an element due to the timeout
Producer: 1 is waiting to transfer...

3. Один производитель - один потребитель

Давайте проверим ситуацию, когда есть один производитель и один потребитель:

@Test
public void whenUseOneConsumerAndOneProducer__thenShouldProcessAllMessages()
  throws InterruptedException {
   //given
    TransferQueue<String> transferQueue = new LinkedTransferQueue<>();
    ExecutorService exService = Executors.newFixedThreadPool(2);
    Producer producer = new Producer(transferQueue, "1", 3);
    Consumer consumer = new Consumer(transferQueue, "1", 3);

   //when
    exService.execute(producer);
    exService.execute(consumer);

   //then
    exService.awaitTermination(5000, TimeUnit.MILLISECONDS);
    exService.shutdown();

    assertEquals(producer.numberOfProducedMessages.intValue(), 3);
    assertEquals(consumer.numberOfConsumedMessages.intValue(), 3);
}

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

Producer: 1 is waiting to transfer...
Consumer: 1 is waiting to take element...
Producer: 1 transferred element: A0
Producer: 1 is waiting to transfer...
Consumer: 1 received element: A0
Consumer: 1 is waiting to take element...
Producer: 1 transferred element: A1
Producer: 1 is waiting to transfer...
Consumer: 1 received element: A1
Consumer: 1 is waiting to take element...
Producer: 1 transferred element: A2
Consumer: 1 received element: A2

Мы видим, что создание и потребление элементов из очереди является последовательным из-за спецификации TransferQueue.

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

В последнем примере мы рассмотрим наличие нескольких потребителей и нескольких производителей:

@Test
public void whenMultipleConsumersAndProducers__thenProcessAllMessages()
  throws InterruptedException {
   //given
    TransferQueue<String> transferQueue = new LinkedTransferQueue<>();
    ExecutorService exService = Executors.newFixedThreadPool(3);
    Producer producer1 = new Producer(transferQueue, "1", 3);
    Producer producer2 = new Producer(transferQueue, "2", 3);
    Consumer consumer1 = new Consumer(transferQueue, "1", 3);
    Consumer consumer2 = new Consumer(transferQueue, "2", 3);

   //when
    exService.execute(producer1);
    exService.execute(producer2);
    exService.execute(consumer1);
    exService.execute(consumer2);

   //then
    exService.awaitTermination(10__000, TimeUnit.MILLISECONDS);
    exService.shutdown();

    assertEquals(producer1.numberOfProducedMessages.intValue(), 3);
    assertEquals(producer2.numberOfProducedMessages.intValue(), 3);
}

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

Producer: 1 is waiting to transfer...
Consumer: 1 is waiting to take element...
Producer: 2 is waiting to transfer...
Producer: 1 transferred element: A0
Producer: 1 is waiting to transfer...
Consumer: 1 received element: A0
Consumer: 1 is waiting to take element...
Producer: 2 transferred element: A0
Producer: 2 is waiting to transfer...
Consumer: 1 received element: A0
Consumer: 1 is waiting to take element...
Producer: 1 transferred element: A1
Producer: 1 is waiting to transfer...
Consumer: 1 received element: A1
Consumer: 2 is waiting to take element...
Producer: 2 transferred element: A1
Producer: 2 is waiting to transfer...
Consumer: 2 received element: A1
Consumer: 2 is waiting to take element...
Producer: 1 transferred element: A2
Consumer: 2 received element: A2
Consumer: 2 is waiting to take element...
Producer: 2 transferred element: A2
Consumer: 2 received element: A2

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

В этой статье мы рассмотрели конструкцию TransferQueue из пакета java.util.concurrent .

Мы увидели, как реализовать программу «производитель-потребитель», используя эту конструкцию. Мы использовали метод transfer () для создания формы противодавления, когда производитель не может публиковать другой элемент, пока потребитель не получит элемент из очереди.

TransferQueue может быть очень полезным, когда мы не хотим перепроизводящего производителя, который будет заполнять очередь сообщениями, что приведет к ошибкам OutOfMemory В такой конструкции потребитель будет диктовать скорость, с которой производитель будет создавать сообщения.

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