1. 概述
在本文中,我们将研究java.util.concurrent包中的DelayQueue构造。这是一个阻塞队列,可用于生产者-消费者程序。
它有一个非常有用的特性——当消费者想要从队列中获取一个元素时,他们只能在该特定元素的延迟到期时获取它。
2. 为延迟队列中的元素实现延迟
我们要放入DelayQueue中的每个元素都需要实现Delay接口。假设我们要创建一个DelayObject类。该类的实例将被放入DelayQueue 中。
我们将字符串数据和延迟毫秒作为和参数传递给其构造函数:
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,我们需要实现生产者和消费者的逻辑。生产者类将队列、要生成的元素数以及每条消息的延迟(以毫秒为单位)作为参数。
然后,当调用run() 方法时,它将元素放入队列中,并在每次放置后休眠 500 毫秒:
public class DelayQueueProducer implements Runnable {
private BlockingQueue<DelayObject> 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();
}
}
}
}
使用者实现非常相似,但它也跟踪使用的消息数:
public class DelayQueueConsumer implements Runnable {
private BlockingQueue<DelayObject> 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 的行为,我们将创建一个生产者线程和一个使用者线程。
生产者将以 500 毫秒的延迟将()两个对象放入队列中。该测试断言使用者使用了两条消息:
@Test
public void givenDelayQueue_whenProduceElement
_thenShouldConsumeAfterGivenDelay() throws InterruptedException {
// given
ExecutorService executor = Executors.newFixedThreadPool(2);
BlockingQueue<DelayObject> 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. 消费者无法在给定时间内消费
假设我们有一个生产者正在生成一个将在10 秒后过期的元素:
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);
请注意,使用者的NumberOfConsumptiondElements的值等于零。
6. 生成立即过期的元素
当延迟消息getDelay() 方法的实现返回负数时,这意味着给定的元素已经过期。在这种情况下,生产者将立即使用该元素。
我们可以测试产生具有负延迟的元素的情况:
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. 结论