Руководство по DelayQueue

Руководство по DelayQueue

1. обзор

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

Имеет очень полезную характеристику -when the consumer wants to take an element from the queue, they can take it only when the delay for that particular element has expired.

2. РеализацияDelayed для элементов вDelayQueue

Каждый элемент, который мы хотим поместить вDelayQueue, должен реализовывать интерфейсDelayed. Допустим, мы хотим создать классDelayObject. Экземпляры этого класса будут помещены вDelayQueue.

Мы передадим данныеString иdelayInMilliseconds в качестве аргументов и в его конструктор:

public class DelayObject implements Delayed {
    private String data;
    private long startTime;

    public DelayObject(String data, long delayInMilliseconds) {
        this.data = data;
        this.startTime = System.currentTimeMillis() + delayInMilliseconds;
    }

Мы определяемstartTime –, это время, когда элемент должен быть потреблен из очереди. Затем нам нужно реализовать методgetDelay() - он должен возвращать оставшуюся задержку, связанную с этим объектом, в заданную единицу времени.

Следовательно, нам нужно использовать методTimeUnit.convert(), чтобы вернуть оставшуюся задержку в правильномTimeUnit:

@Override
public long getDelay(TimeUnit unit) {
    long diff = startTime - System.currentTimeMillis();
    return unit.convert(diff, TimeUnit.MILLISECONDS);
}

Когда потребитель пытается взять элемент из очереди,DelayQueue выполнитgetDelay(), чтобы узнать, разрешено ли возвращение этого элемента из очереди. Если методgetDelay() вернет ноль или отрицательное число, это означает, что его можно получить из очереди.

Нам также необходимо реализовать методcompareTo(), потому что элементы вDelayQueue будут отсортированы по сроку действия. Элемент, срок действия которого истекает первым, остается в начале очереди, а элемент с наибольшим сроком действия - в хвосте очереди:

@Override
public int compareTo(Delayed o) {
    return Ints.saturatedCast(
      this.startTime - ((DelayObject) o).startTime);
}

3. DelayQueue Cсын и производитель

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

Затем, когда вызывается методrun(), он помещает элементы в очередь и засыпает на 500 миллисекунд после каждого помещения:

public class DelayQueueProducer implements Runnable {

    private BlockingQueue queue;
    private Integer numberOfElementsToProduce;
    private Integer delayOfEachProducedMessageMilliseconds;

    // standard constructor

    @Override
    public void run() {
        for (int i = 0; i < numberOfElementsToProduce; i++) {
            DelayObject object
              = new DelayObject(
                UUID.randomUUID().toString(), delayOfEachProducedMessageMilliseconds);
            System.out.println("Put object: " + object);
            try {
                queue.put(object);
                Thread.sleep(500);
            } catch (InterruptedException ie) {
                ie.printStackTrace();
            }
        }
    }
}

The consumer implementation очень похож, но он также отслеживает количество использованных сообщений:

public class DelayQueueConsumer implements Runnable {
    private BlockingQueue queue;
    private Integer numberOfElementsToTake;
    public AtomicInteger numberOfConsumedElements = new AtomicInteger();

    // standard constructors

    @Override
    public void run() {
        for (int i = 0; i < numberOfElementsToTake; i++) {
            try {
                DelayObject object = queue.take();
                numberOfConsumedElements.incrementAndGet();
                System.out.println("Consumer take: " + object);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

4. DelayQueue Тест использования

Чтобы проверить поведениеDelayQueue,, мы создадим один поток-производитель и один поток-получатель.

Производитель поместит в очередьput() два объекта с задержкой 500 миллисекунд. Тест утверждает, что потребитель потребил два сообщения:

@Test
public void givenDelayQueue_whenProduceElement
  _thenShouldConsumeAfterGivenDelay() throws InterruptedException {
    // given
    ExecutorService executor = Executors.newFixedThreadPool(2);

    BlockingQueue queue = new DelayQueue<>();
    int numberOfElementsToProduce = 2;
    int delayOfEachProducedMessageMilliseconds = 500;
    DelayQueueConsumer consumer = new DelayQueueConsumer(
      queue, numberOfElementsToProduce);
    DelayQueueProducer producer = new DelayQueueProducer(
      queue, numberOfElementsToProduce, delayOfEachProducedMessageMilliseconds);

    // when
    executor.submit(producer);
    executor.submit(consumer);

    // then
    executor.awaitTermination(5, TimeUnit.SECONDS);
    executor.shutdown();

    assertEquals(consumer.numberOfConsumedElements.get(), numberOfElementsToProduce);
}

Мы можем заметить, что при запуске этой программы будет получен следующий вывод:

Put object: {data='86046157-e8a0-49b2-9cbb-8326124bcab8', startTime=1494069868007}
Consumer take: {data='86046157-e8a0-49b2-9cbb-8326124bcab8', startTime=1494069868007}
Put object: {data='d47927ef-18c7-449b-b491-5ff30e6795ed', startTime=1494069868512}
Consumer take: {data='d47927ef-18c7-449b-b491-5ff30e6795ed', startTime=1494069868512}

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

Такая же ситуация произошла для второго элемента.

5. Потребитель не может потреблять в данное время

Допустим, у нас есть производитель, который производит элемент, который будетexpire in 10 seconds:

int numberOfElementsToProduce = 1;
int delayOfEachProducedMessageMilliseconds = 10_000;
DelayQueueConsumer consumer = new DelayQueueConsumer(
  queue, numberOfElementsToProduce);
DelayQueueProducer producer = new DelayQueueProducer(
  queue, numberOfElementsToProduce, delayOfEachProducedMessageMilliseconds);

Мы начнем тест, но он завершится через 5 секунд. Из-за характеристикDelayQueue, потребитель не сможет получить сообщение из очереди, поскольку срок действия элемента еще не истек:

executor.submit(producer);
executor.submit(consumer);

executor.awaitTermination(5, TimeUnit.SECONDS);
executor.shutdown();
assertEquals(consumer.numberOfConsumedElements.get(), 0);

Обратите внимание, чтоnumberOfConsumedElements потребителя имеет значение, равное нулю.

6. Производство элемента с немедленным истечением срока действия

Когда реализация методаDelayed messagegetDelay() возвращает отрицательное число, это означает, что срок действия данного элемента уже истек. В этой ситуации производитель немедленно потребляет этот элемент.

Мы можем проверить ситуацию получения элемента с отрицательной задержкой:

int numberOfElementsToProduce = 1;
int delayOfEachProducedMessageMilliseconds = -10_000;
DelayQueueConsumer consumer = new DelayQueueConsumer(queue, numberOfElementsToProduce);
DelayQueueProducer producer = new DelayQueueProducer(
  queue, numberOfElementsToProduce, delayOfEachProducedMessageMilliseconds);

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

executor.submit(producer);
executor.submit(consumer);

executor.awaitTermination(1, TimeUnit.SECONDS);
executor.shutdown();
assertEquals(consumer.numberOfConsumedElements.get(), 1);

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

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

Мы реализовали элементDelayed, который создавался и потреблялся из очереди.

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

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