如何实现一个延迟队列?

281 阅读4分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

第一次参与这种写作活动,请各位大佬随意观看

DelayQueue

可以通过延时队列,执行定时任务.

将定时任务放入队列中,队列会自动排序,当时间为0(到达定时时间),会将该任务放到队列头部,那么我们就可以取出定时任务,然后进行操作.

DelayQueue是Delayed元素的一个无界阻塞队列,只有延迟期满才能从队列中提取元素,队列的头部就是延迟期满后保存时间最长的Delayed元素.如果延迟期没有满,队列头部就没有元素,poll返回null.

当元素的getDelay 方法返回一个小于等于0的值,就会发生到期.

实现Delayed接口需要实现两个方法

image.png

getDelay()这个方法是Delayed的方法

这个方法返回的就是延迟期,当延迟期满元素就会放到队列头部,可以对元素进行取出等操作.

compareTo() 这个方法是Comparable的方法

通过这个方法,将队列中的元素进行排序,通常将getDelay作为排序标准

方法

  • take(): 阻塞方法,直到获取延迟期满的元素并移除

  • put():将指定元素插入队列

    • 和offer一样
  • offer(E):将指定元素插入队列:加锁

    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();
            }
        }
    
  • poll():获取并移除头部延迟期满的元素,如果没有延迟期满的元素,就返回null

  • peel():获取但不移除队列头部,如果队列为空,返回null

  • size(): 返回元素数

  • clear(): 自动移除队列中所有元素

实现

1.填充的元素需要实现Delayed接口,因此可以使实体类实现这个接口,也可以将实体类封装一层再实现,不会破坏实体类的功能性.

public class TaskDelayed implements Delayed {

    private ServicePackTask packTask;

    // 获得延迟时间,用 发送时间-当前时间
    @Override
    public long getDelay(TimeUnit unit) {
//        return unit.convert((packTask.getPushTime().getTime() - System.currentTimeMillis()) / 1000 , TimeUnit.SECONDS);
        return (this.packTask.getPushTime().getTime() - System.currentTimeMillis()) /1000;
    }

    // 进行延时队列内部的比较,延迟时间短的放在头部
    @Override
    public int compareTo(Delayed o) {
        return (int) (this.getDelay(TimeUnit.SECONDS) - o.getDelay(TimeUnit.SECONDS));
    }
}

2.实现生产者和消费者方法

public void provider(ServicePackTask servicePackTask) {
        TaskDelayed taskDelayed = new TaskDelayed(servicePackTask);
        queue.offer(taskDelayed);
    }

生产者比较简单,只要调用方法,就会将元素放入队列中

消费者的话其实就是无限循环读取.首先先判断队列中是否有数据,如果没有就睡眠一段时间再循环(减少CPU压力).

如果队列不为空,就使用take方法(会自动阻塞线程直到有元素的延迟期满)取出元素并对元素进行操作.

public void consumer() {
        while (true) {
            if (0 == queue.size()) {
                System.out.println("完结");
                break;
            }
            TaskDelayed take = null;
            try {
                take = queue.take();
                System.out.println(take.getPackTask().getPushTime());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

3.封装方法

使用两个线程将生产和消费隔开.

为什么要将两个方法放到一起?

因为消费是队列自动计算的,但是如果我们不去触发,就无法对队列中的元素进行操作,所以要将消费放入一个可以出发的地方,就是生产的地方.

但是需要使用多线程将两个方法隔开,否则消费者将无法阻塞生产的方法.

public void start(ServicePackTask servicePackTask) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                provider(servicePackTask);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                consumer();
            }
        }).start();
    }

个人优化

如果每次调用生产都要创建,并且创建一个消费线程来进行消费,这样会大大增加服务器的负担,因此,我们可以通过将消费方法放入类的代码块中来执行,当创建类的实例时,就会触发代码块中的消费逻辑,开始进行消费.

{   // 队列消费:发送到用户中心和插入数据库
    new Thread(new Runnable() {
        @Override
        public void run() {
            ServicePackOrderPlanDO servicePackOrderPlanDO;
            OrderPlanDelayItem take = null;
            while (true) {
                if (0 == queue.size()) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    try {
                        take = queue.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (null != take) {
                        servicePackOrderPlanDO = take.getServicePackOrderPlanDO();
                        // 插入消息表
                        ServicePackSendMessageDO servicePackSendMessageDO = transToMessage(servicePackOrderPlanDO);
                        // 消息订阅
                        subscribeMessage(servicePackSendMessageDO,servicePackOrderPlanDO);
                        // 对plan数据库进行修改状态
                        servicePackOrderPlanDO.setPlanState(SendStateEnum.SEND.getCode());
                        servicePackOrderPlanDO.setUpdateTime(new Date());
                        servicePackOrderPlanDAO.updateServicepackOrderPlan(servicePackOrderPlanDO);
                        message_logger.info("服务包任务计划,修改计划状态成功,计划编号:" + servicePackOrderPlanDO.getPlanNo());
                    }
                }
            }
        }
    }).start();
}