DelayQueue作用
DelayQueue
是 Java 中的一个类,它实现了 BlockingQueue
接口,用于存储实现了 Delayed
接口的元素。Delayed
接口表示具有一定延迟时间的元素,元素只有在指定的延迟时间过去后才能从队列中取出。以下是 DelayQueue
的主要作用:
-
延迟任务调度:
DelayQueue
主要用于实现延迟任务调度。你可以将实现了Delayed
接口的元素(例如,实现了延迟时间计算的任务对象)放入队列中,然后消费者可以按照延迟时间从队列中取出这些元素并执行相应的任务。 -
定时任务: 可以将
DelayQueue
用于定时任务的管理。如果你需要在未来的某个时间点执行某项任务,可以将任务封装成一个实现了Delayed
接口的元素,设定合适的延迟时间,然后将其放入DelayQueue
中。 -
缓存过期策略: 在某些场景下,你可能需要缓存一些数据,并在一定时间后使其失效。通过使用
DelayQueue
,你可以实现一个缓存管理机制,将缓存项封装成实现Delayed
接口的元素,设置合适的失效时间,一旦失效时间到达,缓存项就会被从队列中取出。 -
定时提醒通知: 如果你需要在一定的时间后发送提醒或通知,也可以使用
DelayQueue
。将提醒或通知任务封装成实现Delayed
接口的元素,设定合适的延迟时间,然后放入队列中,到达指定时间后提醒任务就会被执行。
总之,DelayQueue
提供了一种方便的方式来处理具有延迟时间的任务或元素,适用于需要在未来的某个时间点触发操作的场景。
DelayQueue实现原理
DelayQueue
是 Java 中的一个并发集合,它实现了 BlockingQueue
接口,用于存储实现了 Delayed
接口的元素。它的实现原理涉及到多线程同步和优先级队列的概念。
下面是 DelayQueue
的基本实现原理:
-
PriorityQueue 实现:
DelayQueue
的内部使用了一个优先级队列(PriorityQueue
)来存储元素。优先级队列是一种基于堆的数据结构,它保证了每次取出的元素都是具有最小优先级的元素。在DelayQueue
中,这里的优先级是根据元素的延迟时间来确定的,延迟时间越短的元素具有更高的优先级。 -
Delayed 接口: 需要存储在
DelayQueue
中的元素必须实现Delayed
接口。Delayed
接口定义了两个方法:getDelay(TimeUnit unit)
和compareTo(Delayed o)
。getDelay
方法返回元素的剩余延迟时间,以给定的时间单位为基准。compareTo
方法用于比较元素之间的优先级。 -
延迟元素添加和取出: 元素被添加到
DelayQueue
时,会根据元素的延迟时间被放入优先级队列中。在队列中,元素会根据延迟时间的大小进行排序,使得最小延迟时间的元素在队头。当消费者尝试从队列中取出元素时,如果队头元素的延迟时间已过,它就会被移出队列并返回给消费者,否则消费者需要等待直到延迟时间过去。 -
多线程同步:
DelayQueue
是一个并发集合,它支持多线程的安全操作。当生产者添加元素或消费者取出元素时,内部会使用同步机制来保证线程安全。生产者和消费者可以并发地操作DelayQueue
,以实现任务的调度和执行。
综上所述,DelayQueue
的实现原理主要涉及到优先级队列的使用以及基于 Delayed
接口的元素比较和延迟时间的计算。这使得 DelayQueue
成为一种方便的方式来处理具有延迟时间的任务调度和管理。
官方文档
一个无限制的blocking queue的Delayed
元素,其中元素只能在其延迟到期时才被使用。 队列的头 是Delayed
元素,其延迟期满后保存时间。 如果没有延迟到期,那么没有头, poll
会返回null
。 当元素的getDelay(TimeUnit.NANOSECONDS)
方法返回小于或等于零的值时,就会发生getDelay(TimeUnit.NANOSECONDS)
。 即使未使用的元素不能使用take
或poll
,它们另外被视为普通元素。 例如, size
方法返回到期和未到期元素的计数。 此队列不允许空元素。
该类及其迭代器实现了Collection
和Iterator
接口的所有可选 方法。 方法iterator()
中提供的迭代器不能保证以任何特定顺序遍历DelayQueue的元素。
这个类是Java Collections Framework的成员。
数据结构
基于优先级队列
java
private final PriorityQueue<E> q = new PriorityQueue<E>(); //基于优先级队列实现
入队和出队,都是基于优先级队列。
为什么要基于优先级队列实现?
DelayQueue
基于优先级队列的设计有助于实现延迟任务调度和管理。这种设计提供了一些重要的优势,使得处理具有延迟时间的任务变得高效和方便:
-
快速获取最小延迟元素: 优先级队列保证了队列中具有最小延迟时间的元素总是在队头。这使得消费者可以快速地获取即将延迟时间到来的任务,从而实现高效的任务调度。
-
自然排序: 基于优先级队列的设计使得元素的排序和比较变得自然。元素根据延迟时间进行排序,这与任务调度的需求相符。最小延迟时间的任务总是优先执行,这是任务调度中常见的需求。
-
无需手动排序: 由于优先级队列内部自动维护了元素的有序性,开发人员无需手动对延迟任务进行排序操作。这样减少了编码的复杂性和错误的可能性。
-
高效的添加和移除: 优先级队列的实现通常使用堆数据结构,使得添加和移除元素的操作具有较低的时间复杂度(通常为 O(log n))。这对于频繁的任务添加和取出操作非常有利。
-
多线程支持: 优先级队列的实现通常支持多线程操作,这与
DelayQueue
作为并发集合的用途相符。多线程的任务调度和管理得以实现。
综合来看,基于优先级队列的设计使得 DelayQueue
在处理具有延迟时间的任务时具有高效性、自然性和易用性。这种设计方案有助于实现任务调度、定时任务和缓存失效等各种场景中的需求。
说白了就是
1、入队
维持堆,因为优先级队列就是堆。
2、出队
根节点就是时间最小的数据。比如15分钟之后关闭订单,肯定是先过期的订单先被关闭,所以每次从根节点出队数据关闭订单。
数据
队列里的数据,必须实现延迟接口。目的是可以获取剩余时间,然后根据剩余时间是否为小于等于0来判断订单是否过期。
java.util.concurrent.Delayed
java
/**
* A mix-in style interface for marking objects that should be
* acted upon after a given delay.
*
* <p>An implementation of this interface must define a
* {@code compareTo} method that provides an ordering consistent with
* its {@code getDelay} method.
*
* @since 1.5
* @author Doug Lea
*/
public interface Delayed extends Comparable<Delayed> {
/**
* 返回剩余时间
*
* ---
* Returns the remaining delay associated with this object, in the
* given time unit.
*
* @param unit the time unit
* @return the remaining delay; zero or negative values indicate
* that the delay has already elapsed
*/
long getDelay(TimeUnit unit);
}
除了要实现延迟接口的getDelay方法,还要必须实现比较方法。因为优先级队列入队的时候,要维持堆。
java
@Override
public long getDelay(TimeUnit unit) { //返回剩余时间
//剩余过期时间 = 过期时间 - 当前时间
long diff = startTime - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) { //比较时间:插入优先级队列的时候,维持堆
return Ints.saturatedCast(this.startTime - ((DelayObject) o).startTime);
}
完整代码
java
package com.baeldung.concurrent.delayqueue;
import com.google.common.primitives.Ints;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* 延迟对象
*
* @author javaself
*/
public class DelayObject implements Delayed {
private String data; //相当于订单对象
private long startTime; //过期时间
/**
*
*
* @param data 订单对象
* @param delayInMilliseconds 订单过期时间:比如30min过期
* @author javaself
*/
DelayObject(String data, long delayInMilliseconds) {
this.data = data;
this.startTime = System.currentTimeMillis() + delayInMilliseconds;
}
@Override
public long getDelay(TimeUnit unit) { //返回剩余时间
//剩余过期时间 = 过期时间 - 当前时间
long diff = startTime - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) { //比较时间:插入优先级队列的时候,维持堆
return Ints.saturatedCast(this.startTime - ((DelayObject) o).startTime);
}
@Override
public String toString() {
return "{" + "data='" + data + ''' + ", startTime=" + startTime + '}';
}
}
源码分析
入队
java
/**
* Inserts the specified element into this delay queue.
*
* @param e the element to add
* @return {@code true} (as specified by {@link Collection#add})
* @throws NullPointerException if the specified element is null
*/
public boolean add(E e) {
return offer(e);
}
java
/**
* Inserts the specified element into this delay queue.
*
* @param e the element to add
* @return {@code true}
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e); //写数据到优先级队列
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
前面说了,延迟队列是基于优先级队列实现,所以这里的入队就是写数据到优先级队列。
出队
java
/**
* Retrieves and removes the head of this queue, waiting if necessary
* until an element with an expired delay is available on this queue.
*
* @return the head of this queue
* @throws InterruptedException {@inheritDoc}
*/
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null)
available.await();
else {
//获取第一个元素(根节点)的延迟时间
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0) //如果第一个元素的延迟时间小于等于0,则出队列
return q.poll();
//如果第一个元素的延迟时间大于0,则等待delay时间后再次尝试获取
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay); //等待delay时间后再次尝试获取
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
延迟队列的出队和优先级的出队不一样,入队是一样的,但是出队不一样,如果入队和出队都一样,那就没有区别了。那延迟队列的出队是什么样呢?
就是多了一个条件,说白了,就是出队之前,需要检查一下时间是否到期,只有到期的订单才出队。否则,就继续等待,直到有订单到期,才继续出队。
具体细节看源码注释。