延迟队列DelayQueue
DelayQueue概念
DelayQueue是一个***无界***的***BlockingQueue(阻塞队列)***,队列中的元素是以到期时间进行排序的,只有到期的元素才能被取出。
扩展
-
无界队列:
简单来讲,无界队列就是指,当队列满了之后,如果又新增元素,队列会自动扩容;举一反三,有界队列就是队列容量固定不变。
-
阻塞队列:
简单来讲,阻塞队列就是指,当队列元素为空,获取元素的线程会等待队列变为非空,除非线程关闭;队列满时,添加元素的线程也会等待队列可用。同理,非阻塞队列,就是,出现上述情况时,立马返回结果
-
队列排序:
网上很多教程说是对头元素的延迟到期时间最长。这边按照实际业务情况应该是:
元素从尾部插入队列,然后按照时间排序,调用内部的CompareTo方法,最先到期的会存放在对头,每次调用take方法获取头部元素,会根据头部元素是否到期,决定接下来的逻辑。
-
不能将null元素放在这种队列中,会导致所有获取元素的线程阻塞。
适用场景
可用于所有延迟处理的业务场景,比如下单后一段时间未支付更改订单状态,定时任务调度等。
DelayQueue定时任务
DelayQueue只能添加实现了Delayed接口的对象。
-
创建对象,实现Delayed接口,重写方法
@Data //lombok注解,类似get/set方法 public class DelayTask implements Delayed { private Long expireTime;//到期时间 private Object data;//插入的数据 private DelayTask(Long expireTime,Object data){ this.data=data; this.expireTime = expireTime; } //对外提供一个创建任务实例的方法 public static DelayTask buildTask(Long expireTime,Object data){ return new DelayTask(expireTime,data); } @Override public long getDelay(TimeUnit unit) { //此处注意时间转换,必须要转为毫秒数,不然底层方法获取等待延迟时间时,会有隐患 return unit.convert(expireTime-System.currentTimeMillis(),TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) {//此处拿当前元素的到期时间与队列内元素对比,判断存储位置 return (int)(this.getDelay(TimeUnit.SECONDS)-o.getDelay(TimeUnit.SECONDS)); } }
-
创建工具类,定义队列,提供添加元素方法
此处实现ApplicationRunner接口,实现项目启动就启动线程读取队列中的任务。
@Component @Order(2)//设置执行顺序,数字越小,越先执行 public class TaskUtils implements ApplicationRunner { //定义队列 public static final DelayQueue<DelayTask> queue = new DelayQueue(); //添加任务方法 public static void add(Object o,Long expire){ queue.offer(DelayTask.buildTask(expire,o)); } @Override public void run(ApplicationArguments args) throws Exception { //项目启动则启动线程,查询所有的任务 new Thread(new Runnable() { @SneakyThrows @Override public void run() { while (true){//持续读取 System.out.println("开始读取---"); DelayTask take = queue.take(); System.out.println("任务获取时间:"+System.currentTimeMillis()); System.out.println("任务内容:"+take.getData()); } } }).start(); } }
-
创建对象,像队列中塞数据
@Component @Order(1) //设置优先级,先插入任务 public class CreateTask implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println("项目启动完成,即将执行任务"); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND,10); Date expire = calendar.getTime(); TaskUtils.add("测试任务", expire.getTime()); System.out.println("任务插入时间:"+System.currentTimeMillis()); } }
源码分析
public boolean offer(E e) {
final ReentrantLock lock = this.lock;//获取锁
lock.lock();
try {
q.offer(e);//向队列中插入数据,方法内部判断插入的数据是否为null,是的话就抛出空指针异常。
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();//释放锁
}
}
简单解释一下offer()执行流程:
- 获取锁,保证线程安全;
- 向队列中插入元素,元素为null就抛异常;
- 判断当前头部元素是否就是目前新增的元素,是的话就把leader线程设置为null。leader指的是当前操作该队列的线程,如果把leader置为空,说明当前没有线程操作该队列,这很容易理解,因为元素一开始没数据,肯定没有线程操作这个队列,就算有线程获取数据,也因为没有元素,进入了线程阻塞;
- available.signal()方法,唤醒线程,意思就是告诉其他线程,队列现在有数据啦,你们可以自由发挥啦。
- 释放锁,释放资源。
-
take()方法
public E take() throws InterruptedException { final ReentrantLock lock = this.lock;//获取锁--1 lock.lockInterruptibly(); try { for (;;) { E first = q.peek();//获取第一个元素 if (first == null) available.await();//如果第一个元素是空,说明队列没数据,则获取线程进入等待;--2 else { long delay = first.getDelay(NANOSECONDS);//获取当前数据的剩余到期时间--3 if (delay <= 0) return q.poll(); first = null; // don't retain ref while waiting ---4 if (leader != null) available.await();//如果当前已经有线程在操作队列,则该线程等待;---5 else { Thread thisThread = Thread.currentThread(); leader = thisThread; // ---6 try { available.awaitNanos(delay);//等待剩余到期时间,然后执行 --7 } finally { if (leader == thisThread) leader = null; //---8 } } } } } finally { if (leader == null && q.peek() != null) available.signal();// ---9 lock.unlock();// ---10 } }
执行流程说明:
- 获取锁,保证线程安全;
- 获取队列头部第一个元素,如果队首为空,则线程阻塞,说明当前队列没有元素,空队列;这就是为什么一开始说不能插入null元素;
- 如果第一个元素不是空,则获取他的剩余到期时间,getDelay()方法就是我们重写的方法,如果到期时间小于0,说明已经到期,直接返回当前数据;
- 如果获取的第一个元素还未到期,则释放first的引用(first=null),防止内存泄露;
- 判断当前有没有其他线程正在操作该队列,如果leader不是null,说明有线程在操作,设置当前线程阻塞,available.await();
- 如果没有其他线程在操作队列,则将当前线程设置为leader;这个时候如果有其他线程访问这个队列,就会看到leader不是null,进入第5步;
- 线程阻塞,阻塞时间为剩余到期时间;
- 执行结束,将leader设置为null,让其他线程有机会变成leader;
- 如果leader是null,并且队列元素不是空,唤醒其他线程
- 释放锁;