Java延时队列(DelayQueue)的使用及内部源码解析
无敌大目录
前言
Java 在JDK 1.5之后提供了一种实现延时任务的队列,取出的元素是队列中超出规定期限的元素,使用延时队列可以帮助解决以下场景出现的问题(不考虑最佳实现方案,这里主要讲的是DelayQueue的原理)
- 当用户点了个外卖下单之后,在30 min没有付款,自动取消订单
- 轮到你值班,在2小时内没有签到就会发短信、邮件提醒签到
与定时任务不同的是,延时任务是需要某种情况下才会触发的任务,如果没用户下单又或者下单后及时付款这个延时任务就不会执行
延时队列(DelayQueue)的使用方式
要明确一点,放在延时队列的元素,都需要实现一个Delayed
接口,拿一个订单类来举例
首先我们创建一个订单类实现了Delayed
接口,重写了getDelay()
和compareTo()
getDelay()
主要用来判断存入队列的元素是否超时,超时则可以取出数据
compareTo()
主要用来对队列中的元素进行排序
package com.hecl.zhenghe.DelayQueue;
import lombok.Data;
import org.apache.tomcat.jni.Time;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
@Data
public class Order implements Delayed {
private String user;
private String order_id;
private long order_time;
public Order(String user, String order_id, long order_time, TimeUnit timeUnit){
this.user = user;
this.order_id = order_id;
this.order_time = System.currentTimeMillis() + (order_time > 0 ? timeUnit.toMillis(order_time) : 0);
}
@Override
public long getDelay(TimeUnit unit) {
return order_time - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
Order order = (Order) o;
long to = this.order_time - order.getOrder_time();
return to <= 0 ? -1 : 1;
}
@Override
public String toString() {
return "Order{" +
"user='" + user + '\'' +
", order_id='" + order_id + '\'' +
", order_time=" + order_time +
'}';
}
}
写一个测试类,来模拟生成订单和订单超时做出的处理
package com.hecl.zhenghe.DelayQueue;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
public class testDelayQueue {
public static void main(String[] args) throws InterruptedException {
Random random = new Random();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
DelayQueue<Order> orders = new DelayQueue<>();
System.out.println(df.format(new Date()) + "订单A加入队列");
//生成订单A并加入队列
Order order_A = new Order("ALiangX","testA_" +random.nextInt(100),10,TimeUnit.SECONDS);
orders.offer(order_A);
Thread.sleep(5000);
//等待5s生成订单B并加入队列
System.out.println(df.format(new Date()) + "订单B加入队列");
Order order_B = new Order("Jack Ma","testB_" +random.nextInt(100),10,TimeUnit.SECONDS);
orders.offer(order_B);
Thread.sleep(5000);
//再等待5s生成订单C并加入队列
System.out.println(df.format(new Date()) + "订单C加入队列");
Order order_C = new Order("Pony","testC_" +random.nextInt(100),10,TimeUnit.SECONDS);
orders.offer(order_C);
while(!orders.isEmpty()){
//若没有超时的元素,take()会阻塞该线程
Order order = orders.take();
System.out.println("订单因为未支付:" +order.toString() + " 在 " + df.format(new Date()) + "被取出!!" );
}
}
}
我们来执行以下这段代码,以下是运行结果
2021-04-28 21:56:53订单A加入队列
2021-04-28 21:56:58订单B加入队列
2021-04-28 21:57:03订单C加入队列
订单因为未支付:Order{user='ALiangX', order_id='testA_85', order_time=1619618223270} 在 2021-04-28 21:57:03被取出!!
订单因为未支付:Order{user='Jack Ma', order_id='testB_23', order_time=1619618228276} 在 2021-04-28 21:57:08被取出!!
订单因为未支付:Order{user='Pony', order_id='testC_45', order_time=1619618233276} 在 2021-04-28 21:57:13被取出!!
上面代码的意思就是,当订单10s没有支付时,就会从该队列取出订单并取消,就实现了我们对订单超时未支付的处理了
DelayQueue源码解析
我们先来看看DelayQueue里的方法,这里我给大家展示的是我们经常使用到的方法
//看到类定义就知道,放入DelayQueue的对象都必须实现Delayed接口
//DelayQueue主要继承了AbstractQueue和实现了BlockingQueue接口
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
//定义了一个ReentrantLock独占锁来保证队列的线程安全
private final transient ReentrantLock lock = new ReentrantLock();
//定义了一个PriorityQueue,这里说一句,其实对象都是存储到这个队列里的
//DelayQueue主要是对队列内对象的超时处理
private final PriorityQueue<E> q = new PriorityQueue<E>();
private Thread leader = null;
//获取Condition对象
private final Condition available = lock.newCondition();
//DelayQueue<Order> orders = new DelayQueue<>();
//通过这个构造方法我们就可以创建一个延时队列
public DelayQueue() {}
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
//添加对象到队列中
public boolean add(E e) {
//具体还是调用了offer()
return offer(e);
}
//将对象添加到队列中
public boolean offer(E e) {
//线程首先要获取锁才可以执行下面的方法
final ReentrantLock lock = this.lock;
lock.lock();
try {
//调用PriorityQueue的offer方法来将对象加入PriorityQueue队列中
q.offer(e);
if (q.peek() == e) {
leader = null;
//唤醒其它线程
available.signal();
}
return true;
} finally {
//解锁
lock.unlock();
}
}
//具体还是调用了offer()来添加对象
public void put(E e) {
offer(e);
}
//经过一段时间才将对象加入队列
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
//将队列中的第一个对象取出来,队列中就不含有这个对象了
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取队列中的第一个元素
E first = q.peek();
//但是当队列中没有对象,或者队列对象未超时则返回null
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
//要不然直接执行PriorityQueue的poll方法将对象取出
return q.poll();
} finally {
lock.unlock();
}
}
//这个方法也是取出超时对象,与poll()的区别是当没有对象超时的时候,会阻塞当前线程,且唤醒其它线程
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)
return q.poll();
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
//与poll()不同的是这里还传进来了两个参数long timeout, TimeUnit unit
//表示当调用此方法获取超时对象时,若没有对象超时,则会阻塞线程一段时间
//这个时间主要由传进来的这两个参数决定的,在超过这段时间还没对象超时就会直接返回null
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null) {
if (nanos <= 0)
return null;
else
nanos = available.awaitNanos(nanos);
} else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll();
if (nanos <= 0)
return null;
first = null; // don't retain ref while waiting
if (nanos < delay || leader != null)
nanos = available.awaitNanos(nanos);
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
long timeLeft = available.awaitNanos(delay);
nanos -= delay - timeLeft;
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
//获取队列中的第一个对象,不会移除这个对象
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.peek();
} finally {
lock.unlock();
}
}
//返回队列中的对象个数
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.size();
} finally {
lock.unlock();
}
}
//获取队列中的位于第一位的超时对象,若队列中没有对象或没有对象超时则返回null,反不然直接返回该对象
private E peekExpired() {
// assert lock.isHeldByCurrentThread();
E first = q.peek();
return (first == null || first.getDelay(NANOSECONDS) > 0) ?
null : first;
}
//清空队列中存在的所有对象
public void clear() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.clear();
} finally {
lock.unlock();
}
}
//移除队列中的某个对象,成功返回true,失败返回false
public boolean remove(Object o) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.remove(o);
} finally {
lock.unlock();
}
}
}
具体每个方法的作用我都标注在了每个方法的上边了,大家直接看代码就可
从上面的源码我们可以看出,其实储存在DelayQueue中的对象都被存储到了PriorityQueue里,大部分也是调用了PriorityQueue里的方法,DelayQueue只是多了一个对对象超时的处理和使用ReentrantLock来保证线程安全而已,想了解ReentrantLock的可以看看我这篇文章 [全网首发]多线程最全知识万字总结(源码解析 ps:不信你能一次看完,建议收藏)
我们下面重点来讲解这个PriorityQueue里的源码
PriorityQueue使用方式和内部源码解析
概念
PriorityQueue是在JDK 1.5 引入的一个非线程安全先进先出(FIFO——First In First Out)的队列,但是它与普通的FIFO队列不同的是,它可以根据存入对象的某种属性来对存入对象的优先级进行排序,优先级高的放在前面,优先执行
场景:
1.机场优先处理VIP客户的请求
2.操作系统优先处理优先级更高的程序
更通俗一点的说法就是有A、B、C三个线程,优先级是A最高,C最低,C先进入队列等待,这时B来了,由于B的优先级比C高,直接排在C前面了,这时A来了,A比B、C两个优先级都要高,一下就排在了B、C前边,这时队列内的顺序就变成了ABC
放入PriorityQueue里的对象都需要用Comparator
比较器来对存入对象进行排序,这个排序由开发者自定
PriorityQueue的使用方式
放入PriorityQueue的对象不需要实现什么接口
首先我们定义一个Customer类,属性主要包括Name、Level这两个
package com.hecl.zhenghe.DelayQueue;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Customer {
private String name;
private int level;
}
写一个测试类来看看
package com.hecl.zhenghe.DelayQueue;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;
public class testPriorityQueue {
public static void main(String[] args) throws InterruptedException {
//创建一个优先队列,并创建一个比较器重写compare方法
Queue<Customer> PriorityQueues = new PriorityQueue(10, new Comparator<Customer>() {
@Override
public int compare(Customer o1, Customer o2) {
return (o1.getLevel() - o2.getLevel());
}
});
Random random = new Random();
//对象入队,入队是随机的,每添加进一个元素,队列都会根据比较器来进行排序
for(int i = 0;i<10;i++){
int level = random.nextInt(50);
Customer customer = new Customer("zhangsan_" + i,level);
PriorityQueues.add(customer);
System.out.println("入队 " + customer);
}
Thread.sleep(2000);
System.out.println();
//对象出队
while (true){
Customer customer = PriorityQueues.poll();
if(customer == null) break;
System.out.println("出队 " + customer);
}
}
}
执行结果
入队 Customer(name=zhangsan_0, level=43)
入队 Customer(name=zhangsan_1, level=26)
入队 Customer(name=zhangsan_2, level=37)
入队 Customer(name=zhangsan_3, level=6)
入队 Customer(name=zhangsan_4, level=49)
入队 Customer(name=zhangsan_5, level=26)
入队 Customer(name=zhangsan_6, level=16)
入队 Customer(name=zhangsan_7, level=5)
入队 Customer(name=zhangsan_8, level=5)
入队 Customer(name=zhangsan_9, level=31)
出队 Customer(name=zhangsan_7, level=5)
出队 Customer(name=zhangsan_8, level=5)
出队 Customer(name=zhangsan_3, level=6)
出队 Customer(name=zhangsan_6, level=16)
出队 Customer(name=zhangsan_1, level=26)
出队 Customer(name=zhangsan_5, level=26)
出队 Customer(name=zhangsan_9, level=31)
出队 Customer(name=zhangsan_2, level=37)
出队 Customer(name=zhangsan_0, level=43)
出队 Customer(name=zhangsan_4, level=49)
从上面的执行结果我们可以看出来,入队的过程是随机的,但是出队的时候,是根据level来进行排序从小到大进行输出,通过这个优先队列我们就可以先执行优先级别高的任务,接下来我们来看看PriorityQueue内部源码
PriorityQueue源码解析
在这里我们只讲述比较重要的几个方法和几个重要属性
构造器
//默认队列大小
private static final int DEFAULT_INITIAL_CAPACITY = 11;
//存储对象的数组
transient Object[] queue;
//队列中元素的个数
private int size = 0;
//比较器,用于对象的排序
private final Comparator<? super E> comparator;
//对队列操作的次数
transient int modCount = 0;
//默认创建大小为11的队列,且不传入比较器,一般用于存储Integer等基本类型就无需指定比较器
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
//指定队列大小
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
//创建默认大小为11的队列,并传入比较器来对对象进行比较排序
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
//上面的那几种构造器都是调用的这个构造方法来创建队列
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
队列内部实现原理(二叉树小顶堆/大顶堆)
小顶堆
除叶子节点,所有非叶子节点都不大于其左右节点的权值
大顶堆
除叶子节点,所有非叶子节点都不小于其左右节点的权值
那到底是大顶堆还是小顶堆呢?
主要还是通过我们传入的比较器来决定的,这里我们就用小顶堆来给大家举例
offer(E e)
此方法主要是添加元素时使用的方法,每行代码的意思我都标在下面了
还有一个add(E e),其源码如下
public boolean add(E e) {
return offer(e);
}
其主要还是调用了offer方法,所以这里我们主要研究offer(E e)
public boolean offer(E e) {
//如果添加的对象为空时,则抛出空指针异常
if (e == null)
throw new NullPointerException();
//对队列操作次数+1
modCount++;
//获取队列内元素个数
int i = size;
//若队列对象个数比队列长度要长,则调用grow()创建一个新的队列,将原有的元素复制到新队列
//queue对象指针指向新的队列
if (i >= queue.length)
grow(i + 1);
//队列元素个数+1
size = i + 1;
if (i == 0)
queue[0] = e;
else
//这个方法是调整队列内元素位置的一个重要的方法
siftUp(i, e);
return true;
}
/**
* 下面的两个方法是扩展队列的两个方法
*/
private void grow(int minCapacity) {
//获取原先队列的长度
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
//若原先长度<64,则原先长度+2,否则原先长度*2得到新长度
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
//private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//若超过了最大长度则调用hugeCapacity来进行处理
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
排序源码解析
因为底层为二叉树小顶堆的实现,所以每当有一个新的元素加入进来都会调整树的结构,下面就是进行树结构调整的源码
/**
* 下面的三个方法是对插入对象进行排序
* 参数k表示需要排序的对象的位置,初始值一般为所有对象的最后一位
* 参数x表示需要排序的对象
*/
private void siftUp(int k, E x) {
//若传进来的比较器不为空则调用第一个方法、否则调用第二个方法
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
private void siftUpComparable(int k, E x) {
//获取排序该对象的父类比较器,一般适用于基本类型
Comparable<? super E> key = (Comparable<? super E>) x;
//若k的位置,也就是对象的初始位置>0才进行排序,k == 0就不需要排序了,队列中只有一个元素排什么序
//普及个小知识(二叉树父子节点的下标关系)
//parentNo = (sonNo-1)/2
//leftNo = parentNo*2+1
//rightNo = parentNo*2+2
//也就是知道了其中任意一个节点,都可以推算出其父子节点的位置
while (k > 0) {
//这个(k - 1) >>> 1 就相当于 (k - 1) / 2
int parent = (k - 1) >>> 1;
//得到父节点对象
Object e = queue[parent];
//若key所持有的对象与e比较,比e大则直接跳出循环,
//比e小则二者对调,直到key持有的对象比其父对象小为止
//这里所使用的比较器就相当于一个对象调用了它的compareTo来与另一个对象比较
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
//这个(k - 1) >>> 1 就相当于 (k - 1) / 2
int parent = (k - 1) >>> 1;
Object e = queue[parent];
//这里使用的是我们传进来的比较器来对两个对象进行比较
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
我上一幅图给大家感受一下
图一是队列中预先存在的元素,接下来通过offer来加入一个新的元素4
会怎么样
这时4排在最后一个,不符合我们小顶堆的性质,所以我们要调整一下站位,通过int parent = (k - 1) >>> 1;
来获取父节点的位置,与父结点相比较,若比父节点小则与父节点进行交换
直到它比父节点大或者它的下标为0才会停止循环,通过父子节点的交换我们就维护了小顶堆的性质,把最小的数放在了根节点了
poll()
直接上源码解析
public E poll() {
//如果队列内对象数量为0直接返回null
if (size == 0)
return null;
//获取最后一个对象
int s = --size;
//对队列操作次数+1
modCount++;
//获取队首对象
E result = (E) queue[0];
//获取队尾对象
E x = (E) queue[s];
将队尾对象置空
queue[s] = null;
//若对尾对象不为空时,则调用siftDown来对剩余的对象进行排序
if (s != 0)
siftDown(0, x);
return result;
}
排序源码解析
private void siftDown(int k, E x) {
//若比较器不为空则调用传进来的比较器进行排序,为空则调用默认的比较器
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
//普及个小知识(二叉树父子节点的下标关系)
//parentNo = (sonNo-1)/2
//leftNo = parentNo*2+1
//rightNo = parentNo*2+2
//size >>> 1 相当于 size / 2
int half = size >>> 1; // loop while a non-leaf
//当k也就是0,大于half也就是一半的size时才跳出循环
while (k < half) {
//获取左子树的下标
int child = (k << 1) + 1; // assume left child is least
//通过下标获取左子树对象
Object c = queue[child];
//获取右子树的下标
int right = child + 1;
//若右子树下标小于队列中存在的对象个数且左子树比右子树大时c就指向右子树
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
//若对象c比队尾元素x大时才进行交换,否则跳出循环
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
//下面的具体流程和上面差不多,只不过用到的比较器是我们传进去的,这里就不再说明了
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
//size >>> 1 相当于 size / 2
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
上图来给大家解释一下
上面这幅图都向大家解释了一个元素poll以及内部树结构的过程,这里就不再解释了,大家认真看图!!
peek()
我们来看看源码
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
源码很简单,如果队列的元素不为空的话,就直接返回队首的元素,没什么好解释的
remove(Object o)
我们直接来看源码
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
private int indexOf(Object o) {
if (o != null) {
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
return -1;
}
private E removeAt(int i) {
// assert i >= 0 && i < size;
modCount++;
int s = --size;
if (s == i) // removed last element
queue[i] = null;
else {
E moved = (E) queue[s];
queue[s] = null;
siftDown(i, moved);
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
从源码我们可以看出要移除一个对象时,需要通过indexOf
来定位到元素,然后然后调用removeAt
来移除元素,移除完元素后再使用siftDown
、siftUp
来调整树结构,这里就不再赘述了,想要了解的大家往上翻翻
一键三连,冲啊!!!
家人们不要吝啬你们的一键三连啊