DelayQueue学习及定时任务实现

本文详细介绍了Java中的DelayQueue,它是一个无界阻塞队列,用于实现延迟处理任务。DelayQueue中的元素按到期时间排序,只有到期的元素才能被取出。文章通过实例展示了如何创建Delayed对象、使用DelayQueue实现定时任务调度,并分析了offer()和take()方法的执行流程。此外,还讨论了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. 释放锁;
参考资源链接:[live555学习笔记:基础类与核心机制解析](https://wenku.youkuaiyun.com/doc/7f2qddihyc?utm_source=wenku_answer2doc_content) live555库中,HashTable和DelayQueue是两个非常关键的组件,分别用于快速数据检索和延迟任务调度。HashTable基于哈希表算法,能够提供快速的键值对映射,适合用于索引数据的快速查找。而DelayQueue则利用延迟队列特性管理需要定时或延时执行的任务。结合这两个组件,可以有效地优化任务调度和数据管理流程。 举个例子,假使你需要在live555流媒体服务器中处理多个定时任务,比如定时刷新流媒体信息或清理过期的会话数据。你可以使用DelayQueue来存放这些定时任务,每个任务都具有一个唯一标识符(fToken)和一个指定的执行时间。同时,你可以使用HashTable来快速获取特定任务的状态或进行快速的任务查找,以决定是否取消或修改一个任务。 下面是一个简化的代码示例,展示了如何在live555中实现这个流程: ```cpp // 引入live555相关的头文件 #include <liveMedia.hh> #include <BasicUsageEnvironment.hh> #include <HashTable.hh> #include <DelayQueue.hh> // 自定义的定时任务结构体 struct DelayedTask { int fToken; long long fDelayMicroseconds; TaskFunc* fTask; // 任务函数指针 void* fClientData; // 任务需要的数据 DelayedTask* fNext; // 链表中的下一个任务 DelayedTask(int token, long long delay, TaskFunc* task, void* data) : fToken(token), fDelayMicroseconds(delay), fTask(task), fClientData(data), fNext(nullptr) {} }; // 假设这是一个全局的HashTable和DelayQueue实例 HashTable<int, DelayedTask*>* gHashTable; DelayQueue<DelayedTask*>* gDelayQueue; // 任务调度器 class CustomTaskScheduler : public TaskScheduler { public: // 添加任务到DelayQueue void addTask(int token, long long delay, TaskFunc* task, void* data) { DelayedTask* newTask = new DelayedTask(token, delay, task, data); gDelayQueue->scheduleDelayedTask(delay, newTask); gHashTable->Add(token, newTask); } // 其他TaskScheduler的方法实现... }; // 在main函数或其他适当的地方初始化 // ... // 现在可以使用addTask方法添加任务到调度器中了 gTaskScheduler->addTask(1, 5000000, yourTaskFunc, nullptr); ``` 在这个示例中,我们定义了一个`DelayedTask`结构体来封装任务信息,并使用HashTable来管理任务标识符到任务实例的映射。通过DelayQueue,我们能将任务按照预定的时间推入队列中执行。 学习完这些基础知识之后,为了能够更加深入地理解live555的高级用法,你可以查阅《live555学习笔记:基础类与核心机制解析》。这份资料详细解析了live555的各个核心类,包括它们的设计目的、使用方法以及在实际应用中可能出现的问题。通过阅读和实践,你将能够熟练掌握live555的多播、UDP、错误处理等关键技术点,并在实际的流媒体服务开发中灵活运用。 参考资源链接:[live555学习笔记:基础类与核心机制解析](https://wenku.youkuaiyun.com/doc/7f2qddihyc?utm_source=wenku_answer2doc_content)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值