DelayQueue学习及定时任务实现

延迟队列DelayQueue

DelayQueue概念

DelayQueue是一个***无界***的***BlockingQueue(阻塞队列)***,队列中的元素是以到期时间进行排序的,只有到期的元素才能被取出。

扩展

  • 无界队列:

    简单来讲,无界队列就是指,当队列满了之后,如果又新增元素,队列会自动扩容;举一反三,有界队列就是队列容量固定不变。

  • 阻塞队列:

    简单来讲,阻塞队列就是指,当队列元素为空,获取元素的线程会等待队列变为非空,除非线程关闭;队列满时,添加元素的线程也会等待队列可用。同理,非阻塞队列,就是,出现上述情况时,立马返回结果

  • 队列排序:

    网上很多教程说是对头元素的延迟到期时间最长。这边按照实际业务情况应该是:

    元素从尾部插入队列,然后按照时间排序,调用内部的CompareTo方法,最先到期的会存放在对头,每次调用take方法获取头部元素,会根据头部元素是否到期,决定接下来的逻辑。

  • 不能将null元素放在这种队列中,会导致所有获取元素的线程阻塞。

适用场景

可用于所有延迟处理的业务场景,比如下单后一段时间未支付更改订单状态,定时任务调度等。

DelayQueue定时任务

DelayQueue只能添加实现了Delayed接口的对象。

  1. 创建对象,实现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));
        }
    }
    
  2. 创建工具类,定义队列,提供添加元素方法

    此处实现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();
        }
    }
    
  3. 创建对象,像队列中塞数据
    @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());
        }
    }
    
    

源码分析

  • offer()方法
    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()执行流程:

  1. 获取锁,保证线程安全;
  2. 向队列中插入元素,元素为null就抛异常;
  3. 判断当前头部元素是否就是目前新增的元素,是的话就把leader线程设置为null。leader指的是当前操作该队列的线程,如果把leader置为空,说明当前没有线程操作该队列,这很容易理解,因为元素一开始没数据,肯定没有线程操作这个队列,就算有线程获取数据,也因为没有元素,进入了线程阻塞
  4. available.signal()方法,唤醒线程,意思就是告诉其他线程,队列现在有数据啦,你们可以自由发挥啦。
  5. 释放锁,释放资源。
  • 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
            }
        }
    

执行流程说明:

  1. 获取锁,保证线程安全;
  2. 获取队列头部第一个元素,如果队首为空,则线程阻塞,说明当前队列没有元素,空队列;这就是为什么一开始说不能插入null元素
  3. 如果第一个元素不是空,则获取他的剩余到期时间,getDelay()方法就是我们重写的方法,如果到期时间小于0,说明已经到期,直接返回当前数据;
  4. 如果获取的第一个元素还未到期,则释放first的引用(first=null),防止内存泄露;
  5. 判断当前有没有其他线程正在操作该队列,如果leader不是null,说明有线程在操作,设置当前线程阻塞,available.await();
  6. 如果没有其他线程在操作队列,则将当前线程设置为leader;这个时候如果有其他线程访问这个队列,就会看到leader不是null,进入第5步;
  7. 线程阻塞,阻塞时间为剩余到期时间;
  8. 执行结束,将leader设置为null,让其他线程有机会变成leader;
  9. 如果leader是null,并且队列元素不是空,唤醒其他线程
  10. 释放锁;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值