【JAVA】DelayQueue填坑

在上一篇DelayQueue的文章【延迟队列DelayQueue的应用】中,我在初始化系统时将所有DelayTask对象入队,然后开启一个消费者线程循环调用DelayQueue的take()方法取出元素消费,后续系统运行过程中若有生产出新的DelayTask,便调用DelayQueue的put()或add()方法将元素入队,从而达到延时执行的目的

在实践中,遇到几个问题,在此记录一下:

问题1:
当DelayQueue队列中没有元素时,这时入队的元素便位于队列头部,阅读DelayQueue文档,可以看到take()方法:获取并移除头部,在可从此队列获得到期延迟的元素之前一直等待
在这里插入图片描述
因此当队列中只有一个元素时,消费者线程便会根据这个元素的到期时间,进入阻塞状态,假设这个元素的到期时间为60s,那么线程将在60s之后唤醒,那么在60s到期之前,假设新入队一个到期时间为40s的元素,虽然40s比60s先到期,但此时线程已进入阻塞,还是会在60s后进行唤醒,60s后,会先取出60s到期的那个元素进行消费,然后取出40s到期的元素进行消费,那么就会造成这个40s到期的元素任务超时了20s

这是因为在系统初始化的时候便创建了一个消费者线程,因此一旦队列中出现了元素,便会立马去take()、进入阻塞,后续入队的元素虽然会调用Delayed接口中的compareTo()方法根据过期时间来排序,但也不会让已经进入阻塞的消费者线程重新来获取此时过期时间最短的元素

参考以下demo:
元素1过期时间为60s,首先进入队列,消费者线程进入阻塞状态
元素2过期时间为40s,为第二个进入队列的元素
元素3过期时间为10s,为第三个进入队列的元素,入队时会进行排序,元素3位于第二的位置,元素2位于队尾,元素1还是头部
(假设处理延时任务很快,接近于0)
在这里插入图片描述
在这里插入图片描述
60s后,消费者线程消费元素1,元素1出队
在这里插入图片描述
消费完元素1后,立马依次take()取出元素3、元素2进行消费,但是可以看到,元素3超时了50s,元素2超时了20s
在这里插入图片描述
Demo代码如下:
实现Delayed接口的DelayTask

@Data
public class DelayTask implements Delayed {

    /**
     * 开始计时时间 不设置则默认为当前系统时间
     */
    private transient Date taskStartTime = new Date();
    /**
     * 过期时间 不设置则默认1分钟
     */
    private transient long taskExpiredTime = 60 * 1000;

    /**
     * 初始设置开始计时时间
     * taskStartTime 开始时间 [String] [yyyy-MM-dd HH:mm:ss]
     * taskExpiredTime 过期时间 [long] 单位:s
     * @param taskStartTime
     * @param taskExpiredTime
     */
    public void initTaskTime(String taskStartTime, long taskExpiredTime) {
        if(Assert.notEmpty(taskStartTime)) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            try {
                this.taskStartTime = sdf.parse(taskStartTime);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
        this.taskExpiredTime = taskExpiredTime;
        this.taskExpiredTime += this.taskStartTime.getTime();
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(taskExpiredTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        return (this.getDelay(TimeUnit.MILLISECONDS) - ((DelayTask) o).getDelay(TimeUnit.MILLISECONDS)) > 0 ? 1:0;
    }
    
}

元素类:

@Data
public class Element extends DelayTask {

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private String name;

    private Date startTime;

    public void execute() {
        Date execteTime = new Date();
        long timeout = (execteTime.getTime() - startTime.getTime()) / 1000;
        System.out.println(name + "执行任务,执行时间:" + sdf.format(execteTime) + ",超时:" + timeout + "s");
    }

}

DelayQueue工具类:

public class DelayQueueHelper {

    private volatile static DelayQueueHelper delayQueueHelper = null;
    private static DelayQueue<DelayTask> queue = null;

    private DelayQueueHelper() {
    }

    public static DelayQueueHelper getInstance() {
        if(delayQueueHelper == null) {
            synchronized(DelayQueueHelper.class) {
                delayQueueHelper = new DelayQueueHelper();
            }
        }
        if(queue == null) {
            queue = new DelayQueue<>();
        }
        return delayQueueHelper;
    }

    public void addTaskV1(DelayTask task) {
        queue.add(task);
    }

    public DelayQueue<DelayTask> getQueue() {
        return queue;
    }

}

测试类:

public class DelayQueueRunnerDemo {

    public static void main(String[] args) {
        testExecute();

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        DelayQueueHelper queueHelper = DelayQueueHelper.getInstance();
        Date now = new Date();
        System.out.println("系统当前时间:" + sdf.format(now));
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(now);

        //元素1延迟60s执行
        calendar.add(Calendar.SECOND, 60);
        Element e1 = new Element();
        e1.setName("元素1");
        Date startTime_1 = calendar.getTime();
        e1.setStartTime(startTime_1);
        long expiredTime = startTime_1.getTime() - now.getTime();
        e1.initTaskTime(sdf.format(now), expiredTime);

        //元素2延迟40s执行
        calendar.add(Calendar.SECOND, -20);
        Element e2 = new Element();
        e2.setName("元素2");
        Date startTime_2 = calendar.getTime();
        e2.setStartTime(startTime_2);
        expiredTime = startTime_2.getTime() - now.getTime();
        e2.initTaskTime(sdf.format(now), expiredTime);

        //元素3延迟10s执行
        calendar.add(Calendar.SECOND, -30);
        Element e3 = new Element();
        e3.setName("元素3");
        Date startTime_3 = calendar.getTime();
        e3.setStartTime(startTime_3);
        expiredTime = startTime_3.getTime() - now.getTime();
        e3.initTaskTime(sdf.format(now), expiredTime);

        queueHelper.addTaskV1(e1);
        System.out.println("元素1入队,到期时间:" + sdf.format(startTime_1));

        queueHelper.addTaskV1(e2);
        System.out.println("元素2入队,到期时间:" + sdf.format(startTime_2));

        queueHelper.addTaskV1(e3);
        System.out.println("元素3入队,到期时间:" + sdf.format(startTime_3));

    }

    public static void testExecute() {
        new Thread(() -> {
            while(true) {
                try {
                    Element element = (Element) DelayQueueHelper.getInstance().getQueue().take();
                    element.execute();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

测试输出:

系统当前时间:2022-09-27 16:26:04
元素1入队,到期时间:2022-09-27 16:27:04
元素2入队,到期时间:2022-09-27 16:26:44
元素3入队,到期时间:2022-09-27 16:26:14
元素1执行任务,执行时间:2022-09-27 16:27:04,超时:0s
元素3执行任务,执行时间:2022-09-27 16:27:04,超时:49s
元素2执行任务,执行时间:2022-09-27 16:27:04,超时:19s

对于问题1,系统初始化时,可以先将所有要入队的元素按到期时间排序,然后入队,保证入队的时候,第一个入队的元素的到期时间是最短的

问题2:
当系统初始化、消费者线程启动后,在后续往延迟队列中添加元素时,若此时队列中没有元素,则又会出现了以上问题,若此时队列中有元素,消费者线程阻塞当中,假设队列中元素如下,并要将元素4入队:
在这里插入图片描述
入队后:
在这里插入图片描述
当30s后,消费者线程去消费元素4,已经超时20s

解决:
入队时,先判断入队元素的到期时间是否小于头部元素,若小于,则clear()清空队列,先将入队元素入队,然后依次将原来的元素入队,以保证头部元素的到期时间最短

    public void addTaskV2(DelayTask task) {
        DelayQueue<DelayTask> delayQueue = queue;
        if(Assert.notEmpty(delayQueue)) {
            DelayTask queueObj = delayQueue.peek();
            if(queueObj.getTaskExpiredTime() > task.getTaskExpiredTime()) {
                DelayQueue<DelayTask> dataQueue = new DelayQueue<>();
                dataQueue.addAll(delayQueue);
                delayQueue.clear();
                delayQueue.add(task);
                for(Iterator<DelayTask> iterator = dataQueue.iterator(); iterator.hasNext();) {
                    delayQueue.add(iterator.next());
                }
            }else {
                delayQueue.add(task);
            }
        }else {
            delayQueue.add(task);
        }
    }

使用的入队方法,执行后:

系统当前时间:2022-09-27 17:00:13
元素1入队,到期时间:2022-09-27 17:01:13
元素2入队,到期时间:2022-09-27 17:00:53
元素3入队,到期时间:2022-09-27 17:00:23
元素3执行任务,执行时间:2022-09-27 17:00:23,超时:0s
元素2执行任务,执行时间:2022-09-27 17:00:53,超时:0s
元素1执行任务,执行时间:2022-09-27 17:01:13,超时:0s
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

戴陵FL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值